拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

ECharts visualMap 连续型与分段型视觉映射实战

ECharts visualMap 连续型与分段型视觉映射实战

做数据可视化这几年,visualMap是我见过最容易被低估、也最容易被用错的ECharts组件之一。很多人第一次接触它是在 ECharts 官方示例的散点图里,看到右侧那条彩色渐变条能拖动、能筛选,觉得挺酷,复制过来一跑,结果颜色不生效、图例错乱、地图上只显示一片灰。更常见的情况是,项目里明明有大量的数值维度需要表达——门店销售额、城市人口密度、设备温度、订单转化率——却只会用单一颜色把所有数据点涂成一个样,白白浪费了视觉传达的带宽。

visualMap 这个组件,本质上是把数据里的数值维度翻译成视觉通道的翻译器:数值大小可以变成颜色深浅、可以变成圆点大小、可以变成透明度。它解决的是"一张图里同时表达两个甚至三个维度"的问题。适合谁来读这篇?如果你已经能用 ECharts 画出基础的折线图、柱状图、散点图和地图,但一到"想让数据自己说话"的环节就卡壳,那这篇就是写给你的。我会按连续型和分段型两条主线拆参数、讲原理、给可直接抄的配置,再把踩过的坑一条条列出来。

1. visualMap 到底解决什么问题

1.1 从一张门店分布图说起

假设你手上有全国 300 个城市的门店数据,每个城市有经纬度和年度营收。如果只按经纬度画散点图,你能看到门店分布,但完全看不出哪个城市赚钱。这时候有几种解法:给点加标签——300 个重叠的标签直接糊成一团;按营收分系列——分 5 档就要写 5 个 series,代码膨胀还不好调色;用 visualMap——一个配置块搞定,颜色自动跟着营收走。

我早期做地图项目时,用的就是"分系列"的笨办法:把数据按营收区间切成 5 组,每组一个 series,手动配 5 种颜色。缺点是阈值一改就要动代码,颜色过渡生硬,而且没法让用户自己拖拽筛选。换成 visualMap 之后,阈值、配色、交互全部收敛到一个配置对象里,维护成本直接降下来。这不是炫技,是实实在在减少重复代码。

visualMap 的定位就是"数据维度到视觉属性的映射器",它不改变数据本身,只在渲染阶段决定每个数据图元长什么样。理解这一点很重要,因为后面所有参数都是围绕"映射规则"展开的。

1.2 视觉映射的底层逻辑:三段式

不管是连续型还是分段型,visualMap 干的事都可以拆成三步:

  • 取值:从 series 的数据里,挑出一个维度(dimension)拿来做映射依据,比如取第 3 列作为"营收"。
  • 归一化/分段:把取到的值和 min/max 比较(连续型),或者和 pieces 里的区间比较(分段型)。
  • 施加视觉属性:命中的区间对应到 inRange 里的颜色、大小、透明度,没命中的落到 outOfRange。

这里有个容易忽略的点:映射依据的维度是可以指定的。散点图的数据写成[lng, lat, revenue]时,默认取最后一个维度(第 2 维)来做映射,但你可以用dimension显式指定,甚至在数据是对象数组时用字段名去指定,比如dimension: 'sales'。这一点在数据来源五花八门的中后台项目里极其有用,后面第 4 节会展开。

我个人的经验是:先把"我要表达哪个数值维度"这个问题想清楚,再动手写配置。很多人上来就抄示例,示例里 dimension 是默认值,换到自己的数据结构上就错位了,颜色自然乱套。

1.3 连续型还是分段型:先问业务要什么

visualMap 有两大类型,type: 'continuous'和type: 'piecewise',选哪个不该看哪个好看,要看你的业务叙述方式。

维度连续型 continuous分段型 piecewise
视觉表现渐变色带,平滑过渡离散色块,边界清晰
适合场景密度、温度、连续指标等级、档位、评级
用户操作拖拽手柄筛选区间点击色块开关某档
阈值改动改 min/max 即可需要重写 pieces
认知负担需要读渐变色标一眼看出属于哪档

举个判断标准:如果业务方说"这个城市属于 A 级还是 B 级",选分段型;如果说"这片区域的活跃度有多高",选连续型。分档的语义是离散的,用渐变去表达反而增加理解成本。

注意:很多人为了"好看"强行用连续型,结果业务方在评审会上问"这个颜色具体代表多少",答不上来。可解释性永远优先于美观。

2. 连续型 visualMap 的参数逐个拆解

2.1 min、max、dimension:先把尺子定准

连续型的核心是 min 和 max,它们定义了渐变的两个端点。如果不写,ECharts 会自动从数据里取最小值和最大值。听起来很智能,但实际项目里我基本都会显式写上,原因有三:

一是跨图表一致性。同一个大屏里有 4 张图都要表达"0 到 100 的完成率",如果每张图各自取 min/max,颜色含义就不统一了,看的人会误判。显式写死 min: 0, max: 100,全屏配色语义一致。

二是避免离群值污染。数据里如果有一个异常大的值(比如某次测试写入的假数据),自动取值会把整个渐变区间拉到很远,其余数据全挤在色带最左端,看起来全是同一个颜色。这时候我会把 max 设成业务上合理的上限,超出的部分交给 outOfRange 处理。

三是配色的可控性。渐变的两个端点颜色决定了中间所有颜色的观感。min/max 一旦确定,我就能针对性地调 inRange 的 color 数组,而不是每次数据一变就重新配色。

至于 dimension,前面提过它是"取哪一维"的开关。对于二维数组数据[x, y, value],dimension 从 0 开始计数,value 在第 2 位就写dimension: 2;如果数据是对象数组{name, lng, lat, value},可以直接写dimension: 'value'。这个细节我在第 4 节会配着具体图表再讲一遍,因为它是最常见的"颜色不生效"元凶。

2.2 inRange 里能映射哪些视觉通道

inRange 是映射规则的落地处,它接受的属性和 ECharts 图元的视觉属性基本一一对应。常用的有这些:

  • color:图元颜色,最常用,可以传单个颜色或颜色数组做渐变。
  • symbolSize:图元大小,写成[最小值, 最大值]表示按映射结果线性插值。
  • colorAlpha:颜色透明度,可以用来表达"次要维度"。
  • opacity:图元整体透明度,连标签一起生效。
  • colorLightness、colorSaturation、colorHue:在基础色上做明度、饱和度、色相的调整。

这里有个实操技巧:当你想同时表达"营收高低"和"增长快慢"两个维度时,不要都往颜色上堆。我会把营收映射到color(红到绿表示从亏到赚),把门店规模映射到symbolSize,一图两用且互不干扰。如果两个维度都映射颜色,最后就是一团谁也看不懂的调色盘。

注意:symbolSize写成数组时是线性插值,但视觉上人类对面积的感知是非线性的,所以点特别大或特别小时数据会"失真"。我的习惯是把 symbolSize 的范围控制在[6, 30]之间,超过这个范围宁可改用气泡图配合图例。

2.3 calculable、realtime、precision 这些交互参数

calculable: true会在渐变条两端显示出可以拖拽的手柄,用户可以实时筛选区间。这个功能在探索式分析场景里非常好用,用户拖一下就能看到"只看营收 50 万以上的城市",不用后端重查。

配套的还有几个:

  • realtime:关闭它(默认就是 true)后,拖动过程中数据不会实时重绘,松手才更新,数据量大时能省不少渲染开销。
  • precision:控制手柄旁边显示的数字精度,默认会根据 min/max 自动判断,但金额场景建议手动设成 0 或 2,比如precision: 0显示整数,precision: 2显示两位小数。
  • text:色带两端的说明文字,默认是['高', '低'],我一般会改成业务语义,比如['营收高', '营收低']。
  • formatter:自定义数值显示格式,可以加单位、加千分位。

实测下来,当数据点在 5000 个以上时,把realtime关掉、把precision设成固定值的组合,拖动流畅度会有明显改善。这是后话,第 5 节会细说。

2.4 outOfRange 与控件外观

outOfRange 定义的是"不在映射区间内"的数据怎么显示。默认情况下它是个空配置,意味着超出 min/max 的点还是按端点颜色渲染。但如果你希望被筛掉的数据变灰、变透明,就得显式配置:

outOfRange: { colorAlpha: 0.15, symbolSize: 4, opacity: 0.2 }

注意这里两个属性的区别:colorAlpha只影响填充色,opacity影响图元和它的标签整体。想让筛选后的数据"若有若无地留个底",用colorAlpha;想让它彻底退到背景,用opacity。我做过的一个客流分析项目里,就是用opacity: 0.1让不符合条件的商圈淡出,同时保留轮廓,用户能看出"这里还有数据只是被过滤了",体验比直接消失好很多。

控件外观方面,itemWidth和itemHeight控制色带的宽高,orient控制横竖方向,left/right/top/bottom控制位置,这几个参数概念直观,不多展开。唯一要提醒的是:色带的方向要和数据的语义方向一致,如果是"越高越红",色带就要让红色在上方或右方,否则用户会反着读。

3. 分段型 visualMap 的配置套路

3.1 pieces 的多种写法与优先级

分段型的灵魂是pieces数组,每个元素描述一个区间。它的写法有好几种,对应不同的语义:

  • { min: 1500 }:大于等于 1500,没有上界。
  • { min: 900, max: 1500 }:900 到 1500 之间。
  • { lt: 5 }:小于 5。lt是 less than,gt是 greater than,还有一个lte和gte表示带等号。
  • { value: 123 }:精确匹配某个值,适合离散的枚举型数据。
  • { min: 0, max: 100, label: '优秀' }:给区间起个别名,色块旁边显示"优秀"而不是数字。

这几种写法可以混用。写 pieces 时有两个坑值得说:第一,区间不要重叠,重叠时 ECharts 按数组顺序取第一个命中的,结果可能和你想的不一样;第二,记得留边界,比如你写了{min: 0, max: 60}和{min: 60, max: 90},那 60 到底归哪档?建议用{min: 0, max: 59.99}这种显式切断,或者明确接受闭区间规则。

3.2 selectedMode、itemSymbol 与交互细节

selectedMode有两个值,'multiple'和'single',决定用户能不能同时选中多个档位。默认是multiple,点击色块可以多选,选中哪个档位图上就只显示哪个区间的数据。改成'single'后只能单选,适合"二选一对比"的场景。

itemSymbol用来给每个色块前面加个小图标,默认是圆点,可以换成'rect'、'diamond'等,配合itemGap调整间距。这个参数看着不起眼,但在配色块较多的仪表盘上,换成方形色块能让整块图例看起来更整齐。

还有一个容易被忽略的showLabel,它控制色块旁边是否显示数值文本。如果你的 pieces 已经自带 label 名(像"优秀""良好""及格"),有时候反而希望把数字藏起来,只留文字,这时候就把它关掉。

3.3 分段型在业务后台里的典型用法

分段型最大的价值是降低认知负担。我在做一个设备健康度监控面板时,用过这样的 pieces:

pieces: [ { gte: 90, label: '健康', color: '#52c41a' }, { gte: 70, lte: 90, label: '关注', color: '#faad14' }, { gte: 50, lte: 70, label: '预警', color: '#fa8c16' }, { lte: 50, label: '严重', color: '#f5222d' } ]

运营同学不需要去读渐变色带,看一眼颜色就知道哪台设备该派人去看。这就是分段型的典型使用场景:语义是离散的档位,而不是连续的量。

注意:gte和gt在一级上区别不大,但在边界密集的场景(比如 0 到 100 打 10 档)就会产生缝隙或重叠。我的做法是统一用gte配lte,并在心里明确"左闭右闭、错位半个单位",避免出现某个值没有归属的尴尬。

4. 不同图表上挂 visualMap 的差异

4.1 散点图与气泡图:最经典的用法

散点图是 visualMap 的主场,因为散点天然承载多维度。数据写成[x, y, value]时,一个 visualMap 就能让点的颜色跟着 value 变化,一眼看出"哪个区域的值更高"。

option = { visualMap: { type: 'continuous', dimension: 2, min: 0, max: 100, calculable: true, inRange: { color: ['#e6f7ff', '#1890ff', '#003a8c'] }, text: ['高', '低'] }, series: [{ type: 'scatter', symbolSize: 12, data: [[10, 20, 35], [15, 25, 88], [30, 18, 12]] }] }

注意dimension: 2这一行,它是让视觉映射正确工作的关键。如果你的数据里没有第三维,visualMap 就没有依据,颜色自然不会变。

气泡图是在散点图基础上再加一层symbolSize映射。我一般会把"重要性"映射到大小,"健康度"映射到颜色,形成"大而红代表大问题、小而绿代表小正常"的视觉语言。

4.2 地图上的视觉映射

在中国地图这类 geojson 场景里,visualMap 常用于表达区域数值。地图数据和散点数据不一样,它通常长成{name: '北京', value: 2345}这样。这时候 dimension 一般不用写,因为地图默认取 value。

visualMap: { type: 'continuous', min: 0, max: 5000, left: 20, bottom: 20, text: ['高', '低'], inRange: { color: ['#f0f9ff', '#096dd9'] }, calculable: true }

这里有个高频问题:地图上有map系列和scatter系列两个图层时,visualMap 默认会作用于所有系列,结果散点的颜色也被地图的值域影响了。解决办法是给 visualMap 加seriesIndex,明确只说给它管谁。这一点在 4.4 会详细讲。

另一个细节是text的显示位置,地图左上角经常被标题或缩放控件占了,我会把 visualMap 定位到left: 'left'之类的角落,避免遮挡。

4.3 折线图与柱状图上的限制

visualMap 对折线图和柱状图的处理,很多人第一次用会懵:颜色确实变了,但只有数据点或柱子的填充变了,线的颜色不受影响。这是因为 visualMap 作用于"图元"(symbol 和柱体),而折线的线本身属于lineStyle,不参与映射。

所以如果要让整条折线跟着视觉映射变,得另想办法,常见的是用visualMap配合多个 series 分色,或者干脆改用lineStyle.color手动处理。柱状图稍微好一点,柱体本身可以被映射,做"温度带"式的柱状图挺实用。

我的建议是:折线图上慎用 visualMap,它带来的收益有限、限制却不少。真要分色,用分段型 + 多 series 反而更直观。

4.4 seriesIndex 与多系列联合控制

当一张图里有多个系列,visualMap 的seriesIndex就派上用场了。它可以是单个索引、数组,也可以是'all'。默认是'all',这也是很多"意外上色"问题的来源。

我常用的模式是给地图系列和散点系列分别配一个 visualMap:

visualMap: [ { type: 'continuous', seriesIndex: 0, min: 0, max: 5000, inRange: { color: ['#f0f9ff', '#096dd9'] } }, { type: 'piecewise', seriesIndex: 1, pieces: [{ min: 100, label: '重点' }, { max: 100, label: '普通' }], inRange: { color: ['#fa541c', '#8c8c8c'] } } ]

两个 visualMap 各管各的,互不干扰。唯一的代价是画面上会出现两个图例控件,需要安排好位置。

注意:多个 visualMap 同时存在时,如果都设了calculable,拖动一个会实时影响另一个吗?答案是只影响自己管辖的系列。但如果两个 visualMap 的 seriesIndex 有重叠,后者会覆盖前者,出现"拖了没反应"的情况,排查时要先看 seriesIndex 有没有串。

5. 踩坑记录与排查清单

5.1 颜色死活不生效的几种原因

这是 visualMap 报怨榜第一。我总结下来无非四类:

  • dimension 指错了。数据只有两维,你写了dimension: 2,映射找不到依据。解决办法:把数据打印出来,数一数每一行有几个元素。
  • 数据里混了 null 或 NaN。这些值不参与映射,点会保持默认色。检查一下接口返回,尤其是后端把空值序列化成空字符串的情况。
  • series 类型不支持。比如饼图、关系图,visualMap 的映射逻辑和图元类型对不上,配了也没效果。
  • min/max 设错了。数据全在 0 到 1 之间,你把 max 写成 10000,所有点都被压到色带最左端,看起来像没变色。

排查顺序我固定用这套:先看数据结构,再看 dimension,再看 min/max,最后看 seriesIndex。90% 的问题在第一步就能发现。

5.2 visualMap 与 legend、dataZoom 打架

这三兄弟凑在一起经常出事。legend 是系列级别的图例,visualMap 是数值级别的图例,两者同时开启时,画面上会有两块控制区,用户不知道哪个管什么。我的做法是:当 visualMap 是主角时,把 legend 关掉或精简,只留必要的系列说明。

dataZoom 和 visualMap 的冲突更隐蔽。dataZoom 过滤的是坐标轴范围,visualMap 过滤的是数值区间,两者叠加后用户可能怎么拖都看不到数据。解决思路是让它们职责分明:dataZoom 控"看哪一段",visualMap 控"看哪个档"。

还有一个实际的坑:visualMap 的 calculable 拖动会触发 option 重绘,如果配置了 dataZoom 的实时更新,可能出现卡顿叠加。数据量大时,我一般只保留一个可交互。

5.3 性能与渲染问题

数据点超过一万时,visualMap 的渲染开销主要来自两处:颜色计算和重绘。优化手段我试过这几种,按收益排序:

手段效果代价
关闭 realtime拖动不卡松手才更新
改用 large 模式大幅提速部分交互失效
减少 pieces 数量计算量下降档位变粗
固定 precision少一次格式化精度丢失

large: true和largeThreshold的组合是另一个大杀器,它让 ECharts 走批量渲染通道,一万个点也能流畅。代价是symbolSize之类的逐点样式会失效,所以要先确认视觉映射策略能不能接受这个限制。

5.4 常见问题速查表

现象可能原因处理方式
所有点颜色一样min/max 范围过大显式设 min/max
颜色完全不出现dimension 与数据不符核对数据维度
拖动后数据消失outOfRange 未配置补 outOfRange
地图和散点互相影响seriesIndex 默认 all分别指定 seriesIndex
色号看着脏渐变经过灰色区换同色系渐变
图例文字看不清text 被背景吞了调 textStyle 颜色

这张表我贴在工位上贴了小半年,新人来问问题,基本前三行就能命中。

6. 进阶玩法与我的一些实操心得

6.1 动态更新 visualMap

visualMap 是可以在运行时用setOption更新的。常见需求是切换主题或切换指标时,换掉整块映射规则。这里有个细节:只更新 visualMap 时要用setOption的合并语义,如果你传了新的 pieces 但那是一项新数组,ECharts 会整体替换,不会残留旧档位;但如果只改inRange.color,它也会精确合并。想彻底重置,可以传{ visualMap: [] }清空后再设。

我做过一个多指标切换的面板,切换"营收/订单数/客单价"时,visualMap 的 min/max、text、precision 都要跟着变。我的封装方式是定义一个buildVisualMap(metric)函数,每次切换重新生成配置对象再setOption,代码干净也不会漏参数。

6.2 多个 visualMap 共存

前面提过双 visualMap 的用法,这里补充一个布局经验:两个控件一定要分居两侧(比如一个左下、一个右下),千万不要上下紧挨着,否则用户很难判断每个色带对应哪个系列。如果实在放不下,考虑用pieces的label做语义区分,比硬挤位置有效。

还有一个隐藏玩法是,把 visualMap 的seriesIndex指向动态计算出的索引数组,实现"根据当前显示中的系列自动决定映射对象"。这在系列数量可变的模板化图表里很实用。

6.3 我个人的几点体会

最后分享几条我在实际项目里反复验证过的经验,都是文档里不会写的东西。

第一,visualMap 的配色不要闭着眼睛选。渐变色经过灰色区时会显得脏,我一般用同一色系的明度变化,比如#e6f7ff到#003a8c,比红到绿那种跨越色相的渐变耐看太多。要表达"好坏"这种对立语义时,才用红蓝或红绿,并且一定要配文字说明。

第二,给 visualMap 留出至少 120px 的空间,无论横竖。很多人把它挤在角落里,色带又短又窄,手柄都拖不准。数据可视化里,控件的可用性和图表本身一样重要。

第三,分段型的 pieces 不要超过 6 档。超过之后色块本身就成了一道需要解码的题目,用户得来回对照。真需要更多档位,说明数据更适合用连续型,或者该拆成两张图。

第四,也是我踩过最疼的一次:在响应式布局里,visualMap 的定位要用百分比或容器相对单位,用固定像素在大屏上没问题,切到窄屏就跑到画布外面去了。这个问题我在上线前一天才发现,返工成本很高,希望你别重蹈覆辙。

第五,如果你在 Vue 或 React 里封装图表组件,visualMap 的配置最好抽成独立的配置片段,和 series 的配置分开管理。因为它经常需要根据数据分布动态调整,混在 series 里会让 diff 逻辑变得很难看。我现在的做法是单独维护一个visualMapConfig对象,由数据层统一计算 min/max,交给图表层直接使用,职责清晰,改起来也快。

按这个思路走下来,visualMap 就不再是那个"抄了示例但说不清为什么"的组件了。它是你手里表达多维度数据的一把好尺子,尺子的刻度准不准,取决于你对数据的理解和对业务的判断,配置参数只是最后那一步落笔。

返回列表