ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ECharts SVG底图地图:非地理坐标平面图大屏实战

ECharts SVG底图地图:非地理坐标平面图大屏实战 做数据可视化大屏踩过坑的人应该都有体会最难啃的往往不是折线图、柱状图那些控件级配置而是地图。GeoJSON 拿不到、行政区划数据太大、园区平面图没有经纬度坐标、机房布局图只有一张设计稿——这类场景一上来就能把人卡住半天。我最近一个项目就属于最后一种客户给了一张厂区平面矢量图要求在上面按区域上色、按工位打点、按物流路线画流动线还要能点选区域联动右侧面板。没有经纬度没有 GeoJSON只有一张图。最后的方案是用 ECharts 的地图系列把这张 SVG 直接注册成底图。这篇内容就把整套流程摊开讲ECharts 是怎么吃下 SVG 的、SVG 底图需要满足哪些结构条件、坐标点位怎么采、区域名怎么映射、分层怎么叠、大屏适配里 pxtorem 为什么对 ECharts 无效、以及我实际调试过程中踩过的那些坑。适合已经会用 ECharts 画常规图表、但在地图底图这一步被卡住的前端和数据可视化同学也适合做企业级数据可视化大屏、需要把非地理坐标平面图接进图表体系的方案设计者。看完你应该能直接照着搭出一版可运行的 SVG 底图地图。1. 先搞清楚ECharts 地图到底需要什么样的底图1.1 三条底图路线对比ECharts 地图组件的底层其实是一个区域集合 坐标系的模型。它不关心你的底图长什么样只关心两件事每个可交互区域的形状边界是什么以及这个区域的唯一标识叫什么名字。围绕这个模型实际项目里有三条路可以走。第一条是 GeoJSON 路线也就是大家最熟悉的echarts.registerMap(china, geoJson)。它把每个行政区域描述成一组经纬度多边形坐标系是经纬度配合geo.center、geo.zoom使用。优点是数据规范、网上现成资源多、和散点图的经纬度天然对齐缺点是数据体积大一个省级数据动辄几百 KB 到几 MB而且只能画行政边界画不了车间 A 区3 号仓库1 号生产线这种业务分区。第二条是 SVG 底图路线也就是本文的重点。你把一张 SVG 丢给registerMapECharts 会把 SVG 里的每个图形元素解析成一个区域坐标系直接沿用 SVG 的viewBox用户坐标。没有经纬度没有投影所见即所得。这条路适合所有有形状、没地理坐标的场景。第三条是瓦片底图路线。本地 PNG 切片、离线瓦片包这类方案适合做真正的 GIS 应用通常配合 Leaflet、OpenLayers 或者 ECharts 的自定义系列使用。它最贴近真实地理场景但工程复杂度也最高涉及切片方案、层级管理、离线包体积控制。把三条路放一起对比会更清楚维度GeoJSON 底图SVG 底图瓦片底图坐标系经纬度SVG 用户坐标经纬度 投影数据体积大百 KB 至 MB 级小几十 KB 到几百 KB极大切片包图形自由度低只能画多边形边界高任意路径/曲线/圆中依赖切片内容散点定位经纬度直接对齐需采集 SVG 坐标经纬度对齐在线/离线均可数据本地完全离线离线需打包典型场景全国/省市数据大屏园区、机房、产线、楼层真实地图应用1.2 SVG 底图真正擅长什么场景我把它归纳成四类基本覆盖了我见过的绝大多数需求。第一类是园区与厂区平面图有围墙、有道路、有建筑轮廓但没有也不需要经纬度关注点是各区域的人员密度、设备状态、能耗数据。第二类是机房与机柜布局图每一排机柜就是一个矩形区域需要按温度、负载、告警状态着色还需要能下钻到单台设备。第三类是产线与工艺流程示意图管道、传送带、工序节点的路径图重点是流动感和状态色不是地理精度。第四类是楼宇楼层平面图楼层切换本质上是多张 SVG 底图的切换。这四类的共同点是图形来自设计稿往往是 AI、Sketch、Figma 或 CAD 导出的矢量图需要高频交互而且对形状还原度的要求远高于对地理精确度的要求。GeoJSON 在这类需求面前基本无能为力SVG 是最自然的载体。1.3 什么时候别用 SVG 底图有两种情况我会主动劝退。第一种是数据源本身就是经纬度。比如你要展示全国门店分布门店表里存的是经纬度坐标这时候硬做 SVG 底图就得把每个门店的经纬度手动投影到 SVG 画布坐标上工作量巨大而且一旦底图换了就得重算。这种场景老老实实用 GeoJSON配合geo坐标系直接吃经纬度。第二种是需要真实地理精度的场景比如物流路径规划、区域热力分布、跨城市轨迹。这类需求本身带有地理属性SVG 是示意图而非地图硬套会带来长期的维护负担。注意SVG 底图是形状坐标不是地理坐标。一旦你发现自己在反复做经纬度转像素的手工换算说明方案选错了应该回到 GeoJSON 或瓦片路线。2. 原理拆解ECharts 是怎样把 SVG 变成可选区域的2.1 registerMap 的入参形态echarts.registerMap的第二个参数平时大家都传 GeoJSON 对象实际上它同时接受 SVG。规范写法是包一个带svg字段的对象import * as echarts from echarts; // svgText 可以是字符串也可以从 DOM 里取 const svgText document.getElementById(factory-svg).outerHTML; echarts.registerMap(factory, { svg: svgText });接口内部会用DOMParser把这段字符串解析成 XML 文档遍历图形节点把每个节点转成一个区域再拼成 ECharts 内部认识的 GeoJSON 结构。理解这一点很关键因为它解释了很多现象为什么 SVG 里的g分组不会被当成区域、为什么text标签的位置有时候会错乱、为什么 SSR 环境下会直接报错。顺便说一句部分版本也能识别直接传入的 SVG 字符串但依赖版本行为不是好习惯。我建议统一用{ svg: ... }的写法并且在注册后做一次自检echarts.registerMap(factory, { svg: svgText }); if (!echarts.getMap(factory)) { console.warn(SVG 底图注册失败检查字符串是否完整、是否包含 xmlns 声明); }echarts.getMap()拿不到结果八成是字符串被截断、或者xmlns命名空间没声明。这个自检能帮你省掉大量图表空白但我不知道为什么的时间。2.2 图形元素到 region 的映射规则这是整套方案里最容易出错的地方。ECharts 解析 SVG 时会把符合条件的图形元素逐个转成区域转换规则大致是这样path、rect、circle、polygon这类有几何形状的元素都会被处理g只是分组容器本身不会成为区域但它的子元素会被逐个处理text、defs、style、clipPath这些非图形元素会被忽略不会干扰区域结构每个区域的名称来自元素上的name属性。最后一条是重点。设计稿导出的 SVG 里元素通常只有id或者干脆什么都没有name属性是不存在的。而name恰恰是后面tooltip、visualMap、geo.regions、select状态、点击事件回调里params.name的唯一依据。所以清洗环节的第一件事就是给每个区域元素补上可读的name。我需要提醒一句不要指望用id兜底。我实测过几个版本有的能从id里读到值有的读不到行为不稳定。清洗阶段统一写name成本很低收益很大。2.3 坐标系与 viewBox 的对齐逻辑SVG 底图模式下geo组件不启用经纬度坐标系而是直接采用 SVG 的viewBox定义的用户坐标系。也就是说如果一个 SVG 的头部是svg xmlnshttp://www.w3.org/2000/svg width800 height600 viewBox0 0 800 600那么整个地图的逻辑坐标范围就是x ∈ [0, 800]、y ∈ [0, 600]原点在左上角y 轴向下增长。你在上面打一个点data写成[320, 180]这个点就落在 SVG 坐标(320, 180)的位置上。这个结论直接决定了两件事。第一geo.center和geo.zoom的行为变了它们不再表示地理中心点和缩放级别而是对整张 SVG 做平移和缩放缩放基准是画布中心。第二projection、aspectScale这类投影相关配置彻底失效配了也没用因为压根没有经纬度参与计算。还有一个容易忽略的点viewBox的宽高比和 ECharts 容器的宽高比不一致时图形会被拉伸。ECharts 默认会做等比适配但如果你手动设了layoutSize或者容器宽高极端就会出现圆变成椭圆正方形变成长方形的情况。我的做法是让底图的viewBox比例和容器比例尽量接近剩下的交给layoutCenterlayoutSize控制。2.4 geo 与 map series 的绑定关系分层渲染是 SVG 底图方案的核心竞争力而分层的正确姿势是geo组件加map系列的组合。geo负责底图本体、交互状态、漫游缩放map系列负责数据着色通过geoIndex绑定到geo上不再单独创建坐标系。option { geo: { map: factory, roam: true, itemStyle: { areaColor: #12253f, borderColor: #2b4a7a } }, series: [ { type: map, geoIndex: 0, // 关键复用 geo 的坐标系 data: [{ name: A区, value: 92 }] } ] };如果map系列不写geoIndex而是自己写map: factoryECharts 会额外创建一个内部坐标系结果是两层地图叠在一起、边界线双重描边、hover 状态互相打架。这个问题我见过太多次表现是鼠标移上去高亮闪一下又消失本质就是两套坐标系在抢事件。再往上加散点、线、动态效果图层时坐标系依然绑定geoIndex{ type: effectScatter, coordinateSystem: geo, geoIndex: 0, data: [[320, 180, 88], [460, 240, 61]] }一条经验只要页面上的图层超过两层就把所有系列的坐标系统一指向同一个geoIndex让geo成为唯一的坐标系提供者。层级顺序、z值、绘制顺序都通过series数组顺序和zlevel来控制逻辑会干净很多。3. SVG 底图准备与预处理实操3.1 素材获取与结构体检素材来源主要有三种。一是设计团队直接给的矢量源文件这是最好的情况因为可以要求他们在导出时保留图层命名。二是公共领域的矢量素材库比如开源图标集、公共领域剪影素材适合做示意图的装饰元素。三是自己用矢量编辑工具画可控性最高。不管从哪来拿到 SVG 之后我第一件事是结构体检检查四项文件里是否含xmlns声明图形元素是path还是rect/circle混用每个区域元素上有没有可读的标识id或nameviewBox是否存在且数值合理。体检最省事的办法是扔进浏览器的开发者工具里看 DOM 结构比在编辑器里翻图层面板直观得多。如果你看到导出文件里全是path而且每个path的d属性长得像天书这是正常的——矢量工具为了兼容性经常把所有形状都转成路径。这不影响使用。这里要说一句关于素材合规的事底图尽量用自己画的、设计团队交付的或者明确标注可商用的素材。从搜索引擎随手抓的图片来源和授权都不清楚用在正式项目里有风险。这不是技术问题但比技术问题更容易出大事。3.2 用 Python 批量清洗 SVG 并注入 name 属性手工在编辑器里一个个加name属性区域一多就是灾难。我一般写个脚本批量处理。核心逻辑是解析 SVG找出所有图形元素按顺序编号再按预设的名字列表或坐标规则映射名称。from xml.etree import ElementTree as ET SVG_NS http://www.w3.org/2000/svg ET.register_namespace(, SVG_NS) # 区域名按图形元素出现顺序映射实际项目里可以按坐标范围判断 REGION_NAMES [A区, B区, C区, 仓储区, 办公区, 停车场] SHAPE_TAGS {f{{{SVG_NS}}}{t} for t in (path, rect, circle, polygon, ellipse)} def clean(src_path, dst_path): tree ET.parse(src_path) root tree.getroot() idx 0 def walk(node): nonlocal idx for child in list(node): tag child.tag if tag in SHAPE_TAGS: if idx len(REGION_NAMES): child.set(name, REGION_NAMES[idx]) else: child.set(name, f区域{idx 1}) # 去掉没必要的属性减小体积 for attr in (style, class, data-name): child.attrib.pop(attr, None) idx 1 else: walk(child) walk(root) tree.write(dst_path, encodingutf-8, xml_declarationTrue) print(f写入 {dst_path}共处理 {idx} 个区域) clean(raw.svg, factory.svg)如果区域数量多、顺序又不好控制可以改成按包围盒中心点坐标匹配def bbox_center(el): # 简化版只处理 rectpath 需要用 svgpathtools 之类库算 bbox x float(el.get(x, 0)) float(el.get(width, 0)) / 2 y float(el.get(y, 0)) float(el.get(height, 0)) / 2 return x, y实话说路径元素的包围盒计算比较麻烦我更推荐的做法是在矢量编辑工具里给每个区域图层改名然后用图层的名称作为name。现在主流的矢量编辑工具都支持读取和导出元素名称很多情况下改名后导出就自动带上名称属性了比写脚本省事。3.3 精简与压缩控制体积SVG 里最容易膨胀的是路径精度。设计稿导出的路径d属性里动辄保留五六位小数M 123.456789 234.567891 L ...这种数据压缩空间极大。实用的做法有两个一是用矢量工具导出时选择优化/压缩选项减少小数位二是手动把小数位截断到一到两位肉眼几乎看不出差异体积能降三到五成。另外要注意清理无用的东西隐藏图层、被注释掉的结构、重复的defs、巨大的渐变定义。有些导出文件里塞了大量style样式块而 ECharts 解析时并不吃这些样式只是白白增加体积和解析时间。我一般会在清洗脚本里直接删掉style、metadata、desc这几类节点。清理完之后一张中等复杂度的厂区图通常能压到 30KB 到 80KB完全可以内联进 JS bundle 或者作为独立 JSON 异步加载。3.4 坐标点位的采集方法没有经纬度意味着所有散点、标签、连线的坐标都得自己采。这是整个流程里最费时间、也最容易做错的一步。我试过三种办法按推荐度排序。第一种是在页面上做一个取点工具。给图表实例挂一个点击监听把点击位置反算成 SVG 用户坐标并打印出来myChart.getZr().on(click, function (e) { const pt myChart.convertFromPixel({ geoIndex: 0 }, [e.offsetX, e.offsetY]); console.log(JSON.stringify([pt[0].toFixed(1), pt[1].toFixed(1)])); });这里的关键是convertFromPixel配上geoIndex它会自动把画布像素坐标反算回 SVG 用户坐标即使地图处于缩放或漫游状态也准确。这比直接取offsetX/offsetY靠谱得多——后者在roam开启后完全不准我最早就是踩了这个坑采了一版点位结果全偏。第二种是直接用矢量编辑工具读坐标。选中一个元素工具会显示它的位置和尺寸取值即可选中一个点工具会显示该点的坐标。适合点位不多的情况。第三种是在底图里预埋标记点。让设计同学在底图上把关键点位用小圆点标出来导出后脚本遍历这些标记元素读取它们的cx、cy作为坐标同时用name作为点位标识。这个办法最省事也最不容易错前提是能和设计同学配合上。提示坐标采集做完之后一定要把点位数据单独存成一个 JSON 文件不要散落在组件代码里。底图一换只需要重新采集这一份数据逻辑代码完全不用动。4. 从零搭一个可运行的实例4.1 目录结构与依赖我用的是一套最朴素的工程结构不引入框架方便你把逻辑直接搬到自己的项目里。project/ ├── index.html ├── src/ │ ├── main.js │ ├── maps/ │ │ └── factory.js # 导出的 SVG 字符串 │ └── data/ │ └── points.json # 采集好的点位 └── package.json依赖只需要 ECharts 本体。如果你要按需引入记得把地图相关的模块也带上import * as echarts from echarts/core; import { MapChart, EffectScatterChart, LinesChart } from echarts/charts; import { GeoComponent, TooltipComponent, VisualMapComponent } from echarts/components; import { CanvasRenderer } from echarts/renderers; echarts.use([ MapChart, EffectScatterChart, LinesChart, GeoComponent, TooltipComponent, VisualMapComponent, CanvasRenderer ]);这里有个实战经验地图系列和散点系列用的是不同的 chart 模块MapChart不会自动把EffectScatterChart带进来。按需引入的写法下忘了加表现是地图出来了点不出来控制台还没报错。这个坑我建议你在开发初期就用完整引入等功能跑通再切换成按需引入避免两种问题混在一起排查。4.2 页面骨架与容器尺寸容器必须显式给宽高这是 ECharts 的铁律。父级用百分比、子级高度塌成 0 是新手最常犯的错。!DOCTYPE html html langzh-CN head meta charsetutf-8 / title厂区数据可视化/title style html, body { margin: 0; height: 100%; background: #050b18; } #app { position: relative; width: 100vw; height: 100vh; } #map { position: absolute; inset: 0; } /style /head body div idappdiv idmap/div/div script typemodule src/src/main.js/script /body /html用inset: 0配合position: absolute是我比较喜欢的写法比width: 100%; height: 100%少一层依赖父容器只要有一个明确尺寸就行了。4.3 注册地图与 geo 基础配置底图字符串从文件里 import 进来注册之后直接用。注意 SVG 字符串必须是完整的一整段从svg到/svg中间不能缺。import factorySvg from ./maps/factory.js; echarts.registerMap(factory, { svg: factorySvg }); const chart echarts.init(document.getElementById(map), null, { renderer: canvas }); const baseOption { backgroundColor: transparent, geo: { map: factory, roam: true, zoom: 1, layoutCenter: [50%, 50%], layoutSize: 96%, itemStyle: { areaColor: rgba(20, 46, 82, 0.65), borderColor: #3a6ea5, borderWidth: 1 }, emphasis: { itemStyle: { areaColor: rgba(46, 122, 180, 0.85) }, label: { show: true, color: #fff, fontSize: 12 } }, select: { itemStyle: { areaColor: rgba(64, 158, 255, 0.9) }, label: { show: true, color: #fff } }, selectedMode: single, label: { show: false } } };几个配置的作用需要说清楚。layoutSize: 96%让整张底图按容器的 96% 等比铺满留一点边距避免边界线被裁掉。selectedMode: single开启单选配合select.itemStyle就能实现点击区域高亮并保持的效果比自己在点击事件里手写状态管理可靠得多。roam: true允许鼠标拖拽平移和滚轮缩放但要注意它会和页面滚动冲突大屏项目里通常保持开启。底图颜色我用的是低饱和的深蓝系这是大屏的常规配色思路底图压暗、数据提亮视觉重心自然落在数据上。如果你做的是浅色主题就把areaColor换成浅灰蓝、borderColor换成中蓝。4.4 叠加 map series 做数据着色底图铺好之后用map系列上数据。关键是geoIndex: 0。const regionData [ { name: A区, value: 92, online: 128, warn: 3 }, { name: B区, value: 76, online: 96, warn: 1 }, { name: C区, value: 41, online: 54, warn: 7 }, { name: 仓储区, value: 63, online: 22, warn: 0 }, { name: 办公区, value: 18, online: 12, warn: 0 }, { name: 停车场, value: 55, online: 0, warn: 0 } ]; chart.setOption({ ...baseOption, tooltip: { trigger: item, backgroundColor: rgba(8, 20, 38, 0.92), borderColor: #2b4a7a, textStyle: { color: #d8e6f5, fontSize: 12 }, extraCssText: white-space: normal; word-break: break-all; max-width: 240px;, formatter: function (p) { if (!p.data) return ${p.name}br/暂无数据; return [ b${p.name}/b, 负载率${p.data.value}%, 在线设备${p.data.online} 台, 告警数量${p.data.warn} 条 ].join(br/); } }, visualMap: { type: continuous, min: 0, max: 100, left: 24, bottom: 24, calculable: true, text: [高, 低], textStyle: { color: #8fa8c8 }, inRange: { color: [#12365e, #1e6fa8, #2fa8c8, #f2c14e, #e05b4a] } }, series: [ { type: map, geoIndex: 0, data: regionData } ] });visualMap配map系列是标准组合它会按value给区域上色。这里有个细节值得说visualMap的inRange.color数组是从低到高依次插值的五段色阶比三段更能拉开层次但超过七段人眼就分辨不清了我一般控制在四到六段。另外注意formatter里我做了p.data的空判断。原因是有时候底图上的区域没有对应的数据这时候params.data是undefined直接取p.data.online会报错。这个空判断是必须的不是可选项。4.5 叠点、线、动态效果图层底图和数据着色只是一半另一半是业务图层。散点用scatter或effectScatter连线用lines两个都用coordinateSystem: geo加geoIndex: 0绑定坐标系。const points [ { name: 1号工位, coord: [186.5, 242.0], value: 88, status: normal }, { name: 2号工位, coord: [248.0, 242.0], value: 31, status: warn }, { name: 中控室, coord: [420.0, 132.5], value: 96, status: normal } ]; const lines [ { fromName: 仓储区, toName: A区, coords: [[520, 380], [186, 242]] }, { fromName: 仓储区, toName: B区, coords: [[520, 380], [248, 300]] } ]; const businessSeries [ { name: 工位状态, type: effectScatter, coordinateSystem: geo, geoIndex: 0, zlevel: 3, symbolSize: function (val, params) { return params.data.status warn ? 14 : 9; }, rippleEffect: { brushType: stroke, scale: 2.6 }, itemStyle: { color: function (params) { return params.data.status warn ? #e05b4a : #31d0aa; }, shadowBlur: 12, shadowColor: rgba(49, 208, 170, 0.6) }, label: { show: true, position: right, formatter: {b}, color: #c7dcf2, fontSize: 11 }, data: points.map(p ({ name: p.name, value: [...p.coord, p.value], status: p.status })) }, { name: 物流路径, type: lines, coordinateSystem: geo, geoIndex: 0, zlevel: 4, polyline: false, effect: { show: true, period: 4, trailLength: 0.35, symbol: arrow, symbolSize: 7, color: #f2c14e }, lineStyle: { color: rgba(242, 193, 78, 0.55), width: 1.6, curveness: 0.22 }, data: lines } ]; chart.setOption({ series: [...businessSeries] });关于zlevel值得多说两句。ECharts 里zlevel不同的系列会被绘制到不同的画布上好处是分层清晰、局部刷新互不影响代价是每个zlevel都会占用一张独立的画布移动端显存吃紧。我的经验是大屏项目里控制在两到三个zlevel就够了别每个系列都单独开一层。同一层里的前后关系用z控制即可。effectScatter的rippleEffect和lines的effect都是动画静态截图看不出来但实际运行时很吃 CPU。如果页面上有几十个闪烁点建议关掉rippleEffect只保留颜色区分或者把period调大一点降低动画频率。这是性能优化里性价比最高的一刀。4.6 tooltip 换行与交互细节ECharts 的 tooltip 默认是单行不换行的内容一长就会撑成一根横条非常难看。解决办法是让 tooltip 的 DOM 允许换行tooltip: { trigger: item, extraCssText: white-space: normal; word-break: break-all; max-width: 260px; }white-space: normal覆盖默认的nowrapword-break: break-all保证长字符串能断行max-width控制宽度上限。三件套一起上才有效果只写其中一个往往看不到变化。另一个交互细节是点击联动。用geo的select状态配合事件chart.on(click, function (params) { if (params.componentType series params.seriesType map) { console.log(选中区域, params.name, params.data); // 通知外部面板刷新 } }); chart.on(geoselectchanged, function (params) { const selected params.regions.filter(r r.selected).map(r r.name); console.log(当前选中, selected.join(,)); });geoselectchanged是geo组件专有的事件配合selectedMode用起来比自己在click里维护选中数组干净得多。注意区分params.componentType因为点散点也会触发click不做类型判断容易误触发区域切换逻辑。4.7 大屏适配、resize 与 pxtorem 的坑大屏适配这块必须单独讲因为这里有个非常典型的误解。很多人项目里配了pxtorem或者类似的 PostCSS 插件做移动端适配结果发现 ECharts 里的字号纹丝不动、位置也没变。原因是ECharts 默认用 Canvas 渲染Canvas 里的内容是用 JS 在画布上画出来的不经过 CSS 层任何基于 CSS 的 px 转换插件都作用不到它。同理CSS 里的媒体查询、vw/vh单位也影响不到 Canvas 内部。正确的做法有三种。第一种是手动换算在 JS 里自己算出一个缩放系数然后把字号、symbolSize、borderWidth这些值乘上去。function getScale() { const w window.innerWidth; // 以 1920 设计稿为基准 return Math.min(w / 1920, 2); } function buildOption() { const s getScale(); return { geo: { map: factory, itemStyle: { borderWidth: Math.max(0.5, s) }, label: { fontSize: Math.round(12 * s) } }, series: [{ type: effectScatter, coordinateSystem: geo, geoIndex: 0, symbolSize: Math.round(9 * s), data: points.map(p ({ name: p.name, value: [...p.coord, p.value] })) }] }; }第二种是改用 SVG 渲染器echarts.init(dom, null, { renderer: svg })。SVG 渲染器输出的就是真实 DOMCSS 能作用到它pxtorem也能生效。但代价是数据量大时 DOM 节点爆炸几千个散点会让浏览器非常吃力。所以我的判断标准是区域类地图、点位在几百以内可以用 SVG 渲染器换取样式控制力数据量大、有复杂动画的还是老老实实用 Canvas 加手动换算。第三种是整体缩放把整个大屏容器用transform: scale()缩放内部全部按设计稿的固定尺寸写。这个方案最简单粗暴缺点是容器内的文字会被缩放得模糊而且和浏览器原生交互比如输入框会有偏差。适合纯展示型大屏。无论用哪种方案resize都要处理好let timer null; window.addEventListener(resize, function () { clearTimeout(timer); timer setTimeout(function () { chart.resize(); // 如果字号需要跟着变重建 option chart.setOption(buildOption(), { notMerge: false }); }, 200); });加防抖是必须的不加的话拖拽窗口会触发几百次resize画面直接卡死。另外如果用了transform: scale()方案chart.resize()就不需要了因为容器尺寸没变。5. 常见问题排查实录5.1 地图完全空白什么都不显示这是遇到最多的问题按下面顺序查一遍基本能定位。第一确认registerMap是否成功。用echarts.getMap(factory)检查返回undefined就是注册失败。常见原因是 SVG 字符串不完整、缺少xmlns声明或者用构建工具 import 时被当成了模块处理而没导出字符串。第二确认geo.map的名字和注册的名字完全一致。大小写、空格、连字符都算不同。我见过因为一处factory写成Factory排查了半小时的。第三确认viewBox存在。有些导出文件只有width和height没有viewBox解析出来的坐标范围会异常。第四确认容器有真实宽高。在init之前打印document.getElementById(map).clientWidth如果是 0就是 CSS 的问题。第五打开控制台看有没有DOMParser is not defined之类的报错。这个报错说明你在服务端渲染环境里执行了注册逻辑需要挪到客户端生命周期里。5.2 区域点不动、hover 没反应先查有没有开启silent这个是全局静默开关开了就什么都不响应。再查selectedMode是否设置没设置的话点击不会有选中态。然后查图层遮挡如果散点系列的symbolSize很大或者有覆盖全屏的透明元素事件会被上层元素吃掉。还有一个隐蔽的原因底图里存在大量没有name的元素ECharts 把它们也当成了区域hover 时命中的是这些无名区域看起来就像点不动。解决办法是在清洗阶段把不需要交互的装饰性元素比如网格线、装饰边框从底图里剔除或者把它们移到一个单独的、不参与地图解析的图层里。5.3 散点位置整体偏移偏移分两种。一种是缩放后偏移这几乎肯定是坐标采集时用了offsetX/offsetY而不是convertFromPixel。前者拿到的是画布像素坐标只在zoom: 1且没有平移时恰好等于 SVG 坐标一旦漫游就全乱。另一种是始终偏移一个固定量这通常是 SVG 的viewBox原点和容器边距不一致导致的。检查layoutCenter和layoutSize把它们设成默认值[50%,50%]和null试试如果偏移消失说明问题出在布局参数上。还有一种情况是纵轴方向反了。SVG 坐标系 y 轴向下如果供应商给的点位数据是按数学坐标系y 轴向上采的就需要做H - y的转换其中H是viewBox的高度。5.4 缩放漫游相关的怪异表现roam: true之后滚轮缩放会和页面滚动冲突拖拽平移会和文本选择冲突。解决是给容器加overflow: hidden并在需要的时候用roam: move或roam: scale只开放一种操作。大屏场景里我通常用roam: true让用户自由探索但在嵌入页面的小图表里会用roam: move避免用户误滚页面。另外zoom的初始值和roam是叠加的设zoom: 1.6之后再缩放基准是 1.6 倍而不是 1 倍很多人在做重置视图功能时会算错。重置的正确姿势是同时把zoom、center恢复到初始值并setOption而不是只调zoom。5.5 性能与打包体积优化三条经验。第一底图字符串不要内联到主 bundle 里用动态import()异步加载首屏能省几十到几百 KB。第二ECharts 用按需引入地图场景至少能砍掉一半体积。第三动画图层effectScatter的涟漪、lines的流动数量控制在十个以内超过就把动画关掉改成静态样式。还有一点如果同一张底图在多个页面复用把registerMap抽成一个单例模块避免重复注册和重复解析let registered false; export function ensureMap() { if (registered) return; echarts.registerMap(factory, { svg: factorySvg }); registered true; }5.6 问题速查表现象最可能的原因处理动作地图空白注册失败或名字不一致echarts.getMap()自检核对 map 名称控制台报 DOMParser 相关错误服务端执行了注册挪到客户端生命周期区域无 hover装饰元素被当成区域抢占事件清洗底图剔除无 name 的装饰元素点击无选中态未设置selectedMode设置selectedMode: single并配select样式边框双重描边map系列自建了坐标系给 map 系列加geoIndex: 0缩放后散点偏移点位用了画布像素坐标改用convertFromPixel采集点位整体偏移固定值布局参数或 y 轴方向问题重置layoutCenter/layoutSize检查 y 轴tooltip 不换行只设了 max-width补white-space: normal和word-break字号在大屏上不缩放Canvas 不吃 CSS 转换手动算缩放系数或换 SVG 渲染器拖拽窗口卡顿resize 未防抖加 200ms 防抖首屏加载慢底图字符串内联进主包改动态 import滚轮缩放带动页面roam 与页面滚动冲突容器加overflow: hidden或用roam: move6. 几个踩过之后才明白的细节第一个细节关于底图的层数。我一开始图省事把网格、标注、装饰框和业务区域全放在一张 SVG 里结果 hover 的时候经常命中的是背景网格而且装饰框也被当成了可点区域。后来我改成两张图一张纯业务的区域图只有需要交互的区域、每个都带name一张纯装饰的背景图用 CSSbackground-image引进去不参与 ECharts 解析。改完之后事件逻辑一下子就干净了。所以我的建议是底图和装饰一定要分离哪怕多写一行 CSS。第二个细节关于命名规范。区域名是有标识语义的不是给人看的文案。我现在的做法是name用机器友好的英文或编号比如zone_a、zone_b显示给用户的中文名放在data里通过formatter取或者干脆在visualMap之外单独维护一份映射表。这样做的直接好处是将来底图换了、区域调整了改的是数据而不是底图迁移成本低得多。我吃过一次苦头底图重导之后元素顺序变了中文名全串位排查花了大半天。第三个细节关于底图版本管理。SVG 底图是会迭代的设计同学改了布局、加了新区域你手上的点位坐标就全废了。所以底图文件一定要带版本号点位数据也带版本号两者绑定。我现在习惯把factory.v3.svg和points.v3.json放在一起代码里显式引用版本改版时新建一份而不是覆盖。这样出问题的时候能快速回滚也能一眼看出哪份数据配哪张图。第四个细节关于交互反馈的克制。SVG 底图的形状自由度很高所以特别容易上瘾——加发光、加渐变、加描边动画、加阴影最后整个画面糊成一团数据反而看不清。我在做第二个同类项目时就犯过这个毛病。后来给自己定了一条线底图区域只用纯色填充加细边框所有视觉重量都留给数据图层。底图是背景板不是主角这一条在大屏项目里尤其重要。如果你手上的项目正好卡在底图这一步我建议先别急着写完整的业务逻辑用一张最简单的三四十个矩形的测试 SVG 把整条链路跑通注册、geo显示、geoIndex绑定、上色、散点、点击选中。链路通了再换成真实的设计稿底图剩下的就只是数据清洗和坐标采集的体力活了。这套东西真正难的地方从来不是 ECharts 的配置而是底图清洗和坐标对齐这两步把这两步做扎实后面基本不会再出大问题。
返回列表