在做了前两期的大屏项目之后,收到最多的私信就是问地图能不能搬上大屏。说实话,最开始我是有点抗拒的,因为地图可视化看起来简单,但实际上要处理GeoJSON、坐标系、缩放适配、交互联动一整套东西,比普通柱状图折线图麻烦不少。不过等真正做完这版《图书销售展示全国地图可视化》大屏,我反而觉得它是整个系列里性价比最高的一期——视觉冲击力最强,比赛现场最容易让评委眼前一亮,代码量却完全在可控范围内。这篇文章就把我从0到1做这版大屏的完整思路写下来,包含我自己踩过的坑,适合正在准备大数据可视化比赛作品、期末作业,或者单纯想给自己的项目里加一屏地图的同学参考。
1. 为什么用地图展示图书销售?“一屏看全国”的设计逻辑
1.1 图书销售数据的天然地理属性
图书销售这个场景选得很有意思。出版社看的是全国各区域的发行情况,新华书店系统看的是渠道覆盖,电商运营看的是爆款书的省份热度,这些业务需求全都长在一张地图上。相比“华东区销售额同比增长12%”这样的文字描述,地图上用颜色深浅直接把省份销量渲染出来,哪一个省是深红色、哪一片区域颜色发白,观众一眼就能形成判断,不需要在脑子里做二次转换。
我在这个项目里规划了三层数据展示维度:第一层是省份维度的年度累计销量,用地图区域色块呈现;第二层是重点城市的实时订单脉冲,用涟漪散点在地图上做动态扩散;第三层是左右两侧数据面板,用于展示总额、分类占比、地区排名等明细信息。这样一屏下来,既有宏观的空间分布,又有微观的数值细节,信息层级非常清楚。
1.2 地图大屏与普通Dashboard的区别
先说个可能扎心的结论:如果你只是把一张ECharts地图塞进普通网页中间,那它叫“图表页”,不叫“可视化大屏”。大屏和Dashboard的核心区别在于叙事逻辑。Dashboard默认是让人凑近了看数据的,鼠标点来点去;大屏则是让人站在两米外扫一眼就能抓住重点的。
所以设计地图大屏时,要主动放弃一些交互上的精细操作,把精力放在视觉主次的构建上。我这一版的做法是让地图占据画面大概50%的面积,放在中间偏左的位置,右侧面板放辅助数据,顶部是醒目的投影标题“全国图书销售数据实时监测平台”。左边和右边各留出四个上下排列的信息卡片区域,用于放置关键指标和榜单。
1.3 这个场景怎么在比赛和作业里讲出彩
同样的技术,会讲故事的队伍分数就是更高。图书销售地图大屏有一个天然的优势:数据可解释性强。“广东销量最高”可以解释为人口基数大、阅读消费能力强;“西藏颜色偏淡”可以解释为区域渠道覆盖不足,下一步可以针对性做推广。在比赛答辩时,评委最喜欢听到的就是对数据的解读能力,而不是单纯背一句“使用了ECharts地图组件”。
我建议你准备作品时,提前编好两三个数据故事:一是在地图上找出一匹黑马省份,解释它为什么增长快;二是对比相邻省份的差异,给出业务建议。这会让你的作品从“作业”直接抬升到“作品”的层次。
2. 技术选型:HTML+CSS+JS凭什么撑起一个可视化大屏
2.1 为什么还要回归原生三件套
现在可视化领域早就被Vue、React等框架占领了,但我在做比赛型和作业型大屏时仍然坚持用原生HTML+CSS+JS,一个重要原因是现场可靠性。比赛现场的电脑是什么环境谁也说不准,没有Node环境、装不了依赖、网络受限,这些都是常见事故。原生三件套只要有一个浏览器就能跑,双击index.html就能打开,零部署成本。
而且从学习价值来讲,用原生技术完成一版复杂大屏,你对CSS布局、JS数据处理、异步加载的理解会明显比套用框架更扎实。框架帮你藏起来的那些细节,恰恰是大屏最容易出问题的位置。等原生版本跑通了,再迁移到Vue里也就是Ctrl+C、Ctrl+V的事。
2.2 ECharts引入方式与版本选择的建议
地图渲染部分我选用了ECharts 5.4.3。选它的理由很直接:生态成熟、中文文档齐全、开箱即用的地图支持。在国内做可视化大屏,ECharts基本是事实标准,评委和老师看到它不会觉得陌生,也方便你在答辩时解释原理。
引入方式建议用Script标签直接引CDN,不要用npm。比赛现场很可能没有网络,所以更稳妥的做法是提前把echarts.min.js下载到本地js目录里。引用版本要固定,不要用“最新”这种不稳定的写法,避免未来ECharts升级导致配置项语法变化,项目放半年后再跑突然报错。
<script src="js/echarts.min.js"></script> <script src="js/china.js"></script> <script src="js/index.js"></script>这里注意china.js就是地图GeoJSON数据文件,它不是ECharts框架本身,而是单独的一份地图数据脚本。引入顺序必须在echarts.min.js之后,因为china.js内部会调用ECharts的注册接口。
2.3 项目文件结构与源码组织
大屏项目最忌讳把所有代码堆在一个HTML里,那会让后期维护变成灾难。我建议的目录结构是这样的:
book-sales-dashboard/ ├── index.html # 主页面 ├── css/ │ ├── base.css # 全局样式与重置 │ ├── layout.css # 大屏布局与面板 │ └── animation.css # 动画和装饰效果 ├── js/ │ ├── echarts.min.js # ECharts核心库 │ ├── china.js # 中国地图GeoJSON │ ├── mockdata.js # 模拟数据生成器 │ ├── map.js # 地图核心渲染逻辑 │ └── panels.js # 侧边面板图表逻辑 ├── images/ │ └── bg.png # 背景纹理这样拆分的逻辑是:数据、渲染、布局三者解耦。比赛现场如果需要临时改数据,只需要编辑mockdata.js,不需要去渲染代码里翻找。如果有同学问你“数据源在哪”,你也可以直接指出这个文件,体现你的工程意识。
3. 大屏布局的搭建:从设计稿到像素级还原
3.1 1920x1080基准与自适应缩放方案
大屏开发的第一步是确定基准分辨率。绝大多数大屏硬件是1080P或4K,我习惯以1920x1080作为设计基准,然后用一套等比缩放的逻辑去适配其他分辨率。这里最推荐的方式是在最外层容器上用transform: scale()做整体缩放。
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #030b1a; } #screen { width: 1920px; height: 1080px; transform-origin: 0 0; overflow: hidden; position: relative; }配合一段JS,在页面加载时计算缩放比例,让整个大屏等比例适配到任意屏幕上:
function autoScale() { const screen = document.getElementById('screen'); const scaleX = window.innerWidth / 1920; const scaleY = window.innerHeight / 1080; const scale = Math.min(scaleX, scaleY); screen.style.transform = 'scale(' + scale + ')'; } window.addEventListener('resize', autoScale); autoScale();这种方案的好处是整个1920x1080画布里的定位逻辑完全不用改,写死像素坐标也不会乱。注意transform-origin: 0 0必须加,否则缩放会以元素中心为基准,左上角对不齐。
3.2 CSS 3D变换:rotateY与translateZ的实际效果
标题和装饰动画是大屏氛围感的重要来源。很多同学看到CSS里面写transform: rotatey(60deg) translatez(300px)就懵了,不知道出来是什么样子。我实际试过之后可以给你一个直观描述:这个复合变换的含义是,先让元素绕Y轴旋转60度,然后沿着变换后自身坐标系的Z轴方向平移300px。结果就是元素看起来像一扇被推开一半的门,并且相对于屏幕向外“飘”出了一段距离。
如果你想让大屏在加载时产生“数据从远处飞来”的入场效果,可以把它用在卡片或数字层的动画里。配合父容器设置perspective,空间感才会出来:
.loader-wrapper { perspective: 1000px; } .rotate-tip { transform: rotateY(60deg) translateZ(300px); transition: all 1s ease; } .rotate-tip.active { transform: rotateY(0deg) translateZ(0px); }这里最容易被忽略的点是:perspective尽可能加在父元素上,而不是动画元素本身,否则每个子元素会以自己为原点建立透视,视觉上会非常奇怪。想让3D旋转看起来正常,这是一个核心规则。
3.3 装饰边框、标题栏与涟漪光圈
大屏如果没有装饰元素,再好的地图也会显得干瘪。我的做法是在面板边角加CSS绘制的发光角标,同时做一条顶部流动光效的标题栏。标题栏可以拆成三段:中间主标题、左右两侧的对称光影。这样视觉重心稳定,观众视线不会被引导到旁边去。
涟漪光圈是另一个强烈推荐的装饰效果。ECharts的effectScatter只能在地图上做涟漪,如果面板上的某个重点数字也想要雷达扫描的效果,可以完全用CSS实现:
.ripple { position: relative; width: 80px; height: 80px; border-radius: 50%; border: 2px solid rgba(64, 158, 255, 0.8); } .ripple::after { content: ''; position: absolute; inset: 0; border-radius: 50%; border: 2px solid rgba(64, 158, 255, 0.5); animation: ripple-diffuse 1.5s ease-out infinite; } @keyframes ripple-diffuse { 0% { transform: scale(1); opacity: 1; } 100% { transform: scale(2.5); opacity: 0; } }用::after伪元素配合scale放大和透明度衰减,就做出来了雷达波纹效果,不需要额外加载任何库。
4. 地图可视化核心实现:全国地图上的图书销售数据
4.1 地图注册与数据来源
ECharts本身不内置中国地图数据,你需要先注册一份GeoJSON。我用的是ECharts官方配套的china.js,加载完这个文件后,ECharts全局环境中就已经存在名为china的地图注册项了。
在JS里初始化项目时,你可以二次确认注册是否成功:
const registered = echarts.getMap('china'); if (!registered) { console.error('中国地图未加载成功,请检查 china.js 是否在 echarts 之后引入'); }这个检查很有必要。很多同学的地图大屏为什么不出图?十次里有八次都是因为china.js没加载成功或者顺序错了,而浏览器控制台又不会给出明确的报错,只有一句Component series.map not exists类似的信息,排查起来很折磨人。
4.2 用地图series渲染省级销售分布
核心的地图配置项我放在一个函数里管理。这里要注意的是visualMap,它决定了颜色深浅和数值范围的映射关系,也是地图能表达数据分布的关键。
function initMap() { const mapDom = document.getElementById('mapContainer'); mapChart = echarts.init(mapDom); const option = { tooltip: { trigger: 'item', formatter: function(params) { if (params.value) { return params.name + '<br/>销量:' + params.value.toLocaleString() + ' 册'; } return params.name + '<br/>暂无数据'; } }, visualMap: { min: 0, max: 8000, left: 30, bottom: 30, text: ['高', '低'], calculable: true, inRange: { color: ['#e0f3ff', '#74b9ff', '#0984e3', '#6c1fbf'] } }, series: [{ type: 'map', map: 'china', roam: false, zoom: 1.1, label: { show: true, fontSize: 10, color: '#fff' }, itemStyle: { borderColor: '#00d4ff', borderWidth: 1.2, areaColor: '#0a2b4b' }, emphasis: { label: { color: '#fff', fontSize: 14 }, itemStyle: { areaColor: '#ffd700', shadowBlur: 20, shadowColor: 'rgba(255, 215, 0, 0.6)' } }, data: salesData }] }; mapChart.setOption(option); }这段配置里有个值得注意的细节:visualMap的max我没有设成一个固定的大数字,而是根据数据最大值动态计算,避免某一项的数值过高导致整个地图颜色过深、失掉层次。具体做法就是在生成模拟数据后取一次Math.max。
4.3 数据格式和省份名称对齐
ECharts地图的数据难点永远不是渲染,而是数据里的省份名称与地图GeoJSON里的名称完全匹配。china.js里的省份名称是“广东”“北京”“新疆维吾尔自治区”这种全称,如果你的数据源里写的是“广东省”“北京市”,地图上就会对应不上,表现为某省份一直显示“暂无数据”。
所以在数据处理环节,我专门写了一个名称归一化函数:
function normalizeProvinceName(name) { return name .replace(/省$/, '') .replace(/市$/, '') .replace(/壮族自治区$/, '') .replace(/维吾尔自治区$/, '') .replace(/回族自治区$/, '') .replace(/特别行政区$/, ''); }用replace把常见后缀去掉,保证数据能正确落在地图区域上。这个函数虽然小,但没有它,你的地图就永远是残缺的。
4.4 涟漪散点图叠加重点城市销售脉冲
只做区块填色,画面会显得安静。我更推荐在地图上叠加一个effectScatter系列,让几个重点城市的位置出现涟漪扩散效果,模拟“正在发生”的销售脉冲。这个系列在视觉上给评委的感知非常直接,像雷达波一圈一圈往外推。
const cityData = [ { name: '北京', value: [116.46, 39.92, 3200] }, { name: '上海', value: [121.48, 31.22, 2800] }, { name: '广州', value: [113.23, 23.16, 2600] }, { name: '成都', value: [104.06, 30.67, 2200] } ]; series.push({ type: 'effectScatter', coordinateSystem: 'geo', data: cityData, symbolSize: 10, showEffectOn: 'render', rippleEffect: { scale: 4, brushType: 'stroke' }, itemStyle: { color: '#00ffff', shadowBlur: 10, shadowColor: '#00ffff' } });坐标系的配置要在geo字段里先定义好。这里有个容易踩的点:如果地图series里没有配geo,而是直接类型为map,那么effectScatter上面的coordinateSystem: 'geo'就会报错。我在项目里的做法是在option里同时配置geo和series[map],geo专门负责坐标系,map系列负责区域染色,二者互不干扰。
5. 数据层处理:模拟数据、格式化与动态刷新
5.1 用JS模拟数据的合理套路
真实项目里地图数据来自后端接口,但作业和比赛场景通常只能造数。造数也是个技术活,不能让数据看起来太假。比如销量最高的省份不应该总是广东浙江,相邻省份之间也不该出现极端跳变。
我写模拟数据时先定一个基础量,再叠加地区系数。地区系数可以反映人口和经济水平,这样造出来的数据才有业务逻辑。
function generateMockData() { const base = { '广东': 7800, '江苏': 6900, '山东': 6100, '浙江': 5800, '河南': 5300, '四川': 4900, '湖北': 4300, '湖南': 3800, '北京': 7200, '上海': 6800, '安徽': 3500, '福建': 3300, '陕西': 2700, '重庆': 2800, '辽宁': 2500, '河北': 2400 }; // 其他省份的默认值统一做一个低基数随机 const national = ['天津','山西','内蒙古','吉林','黑龙江','江西','广西','海南','贵州','云南','西藏','甘肃','青海','宁夏','新疆']; const result = []; national.forEach(function(item) { result.push({ name: item, value: Math.round(800 + Math.random() * 1500) }); }); Object.keys(base).forEach(function(item) { result.push({ name: item, value: Math.round(base[item] * (0.85 + Math.random() * 0.3)) }); }); return result; }这样生成的销量最大值不会超过8000太多,地图颜色分布跨度可控,每个省份又都有细微差异,演示起来很自然。
5.2 数据格式化与字符串判断技巧
除了销量数字,大屏上还会显示诸如“已覆盖省份”“热销品类”等文案。数据处理阶段经常会用到字符串判断和格式化,比如从一段描述里判断省份列表是否包含某个地区。
const coverStr = '北京|上海|广东|浙江|江苏|四川'; if (coverStr.includes('广东')) { console.log('华南区域已覆盖主销省份'); }includes是判断字符串是否包含子串最直接的API,比indexOf !== -1的可读性好很多。大屏演示时我还会把大数字转成带千分位分隔符的小写格式,直接用toLocaleString()完成,代码干净又不容易出错。
5.3 模拟实时刷新,让大屏“动”起来
大屏最忌讳静态不动。评委站到屏幕前如果发现数字纹丝不动,会立刻觉得是假数据。我加了两个维度的动态效果:一是地图上的涟漪散点每5秒改变一次数值强度,二是侧边排行榜每10秒做一次平滑动效展示变化。
setInterval(function() { const randomIndex = Math.floor(Math.random() * salesData.length); salesData[randomIndex].value = Math.round(salesData[randomIndex].value * (0.9 + Math.random() * 0.24)); mapChart.setOption({ series: [{ data: salesData }] }); }, 5000);这里的setOption只需要传入变化的部分,ECharts内部会做增量更新,不会因为频繁刷新导致闪烁或卡顿。有一点要注意:visualMap的max不要随每次刷新都重新计算,否则地图色板的颜色层次会不停跳变,观感很差。
6. 踩坑实录:地图不显示、缩放糊、3D转不动
6.1 地图不显示,问题多半在加载顺序
我调试这版大屏时,最崩溃的就是地图区域一片空白。排查链路我建议按顺序走:先在浏览器Network面板确认china.js有没有加载成功,再看Console有没有报错,最后看echarts.getMap('china')是否存在。一大半的空白问题都是资源没加载进来。
还有一种是跨域导致的地图加载失败。如果你单独把GeoJSON放在一个.json文件里,再用fetch读取,在file://协议下会直接被浏览器拦截。我后来统一改成把GeoJSON封装成china.js脚本,用全局变量暴露出来,彻底绕开跨域问题。
6.2 小屏笔记本上糊屏,别让scale扛所有事
大屏项目做完后在同事的小笔记本上演示,经常发现字体和地图边缘发虚。原因是transform: scale()缩放时浏览器是按比例整体缩放的,低于1的缩放值会让一些细线被采样成模糊的半色调。
这是个纯CSS问题,处理思路是不要指望一个等比缩放吃遍所有分辨率。更稳的适配方式是做两版布局断点:分辨率大于1600px时用1920基准的缩放方案;低于1280px时切换紧凑布局,把右侧面板的宽度收窄,地图缩小。比赛场地不固定,这个提前量一定要留出来。
6.3 容器隐藏状态下图表初始化宽度为0
大屏经常会做分步加载或者等待动画,地图容器初始状态可能被CSS设置为display:none或宽度为0,等动画结束再显示出来。这时候你在隐藏状态下init地图,ECharts会认为容器宽高是0,渲染出一个空地图或尺寸错乱的地图。
解决方案是在容器变为可见后主动调用一次resize(),但我更推荐的做法是动画完成后延迟初始化地图:
setTimeout(function() { initMap(); }, 600);等入场动画跑完、容器已经拿到实际尺寸后再创建图表,就完全不会出现这个问题。如果你已经初始化了,补救手段是:
mapChart && mapChart.resize();6.4 3D旋转不生效,多半是透视属性缺失
CSS的3D效果非常依赖perspective和transform-style: preserve-3d。我见过很多同学把rotateY(60deg)写在元素上,但是父元素没有设置perspective,导致旋转看起来只是一个扁平的伸缩变化,完全没有空间深度。这个不是浏览器兼容问题,而是CSS 3D坐标系没建立起来。
另外要注意层叠上下文。如果动画元素被overflow: hidden的父容器裁剪,或者z-index设置不当,3D飞入效果会被后面的面板挡住。大屏的层级顺序建议统一为“背景层 -> 地图层 -> 面板层 -> 顶层动效”,按这个顺序分配z-index,就不用反复试position了。
7. 完整代码结构与比赛演示的加分技巧
7.1 源码文件的核心分工
文末提供的源码包括完整的index.html、css目录、js目录和images目录。你拿到后首先打开mockdata.js看数据结构,再打开map.js看渲染逻辑,最后回index.html里对照布局结构,基本一个晚上就能吃透改完。
源码里我把地图主文件map.js注释写得非常细,每个配置项后面都标了作用。这样不仅方便你二次开发,比赛答辩时也直接可以当成代码讲解的底稿,让评委觉得你的工程习惯很好。
7.2 比赛答辩时怎么把这个作品讲清楚
做比赛项目,光做得好不够,讲得好同样重要。我建议答辩流程按“三句话”组织:第一句话讲业务,图书销售数据分布在各省,要用一张大屏实时掌握全国市场;第二句话讲技术,前端基于HTML+CSS+JS原生开发,地图渲染采用ECharts的map系列加effectScatter联动,数据层使用模拟动态刷新模拟实时接入;第三句话讲亮点,颜色深浅代表销量梯度、涟漪散点代表城市热卖、左右面板提供明细支撑。
演示时一定要先让地图出场,停留至少五秒不要操作,让评委看清数据分布。然后再手动刷新一次数据或切换面板,展示动态效果。节奏比内容更影响印象分。
7.3 拿到源码后可以继续扩展的三件事
一是接入真实接口,把mockdata.js里的数据源替换成fetch请求,接口返回同样的数组结构即可;二是做省份下钻,点击某个省跳转到该省的城市地图,需要额外准备城市级GeoJSON,核心逻辑和省级地图完全一致;三是加入自动轮播高亮,定时把某几个重点省份通过dispatchAction高亮播放,增强大屏的“演示感”。
我个人还是最推荐完成省份下钻这个扩展。它能把你的地图大屏从“展示作品”提升到“完整产品”,也是比赛评委最喜欢追问的功能。技术上不复杂,但需要你对地图数据联动有更系统的理解。
最后分享一个我每次演示前必做的小事情:检查浏览器缩放比例是不是100%。浏览器缩放只要是90%或者125%,大屏的等比适配就会产生一圈白边或者裁掉边缘信息,我在现场已经见过太多团队因为这个小问题翻车。代码写得好不如演示保护好,这句话在比赛现场永远成立。