ARTICLE DETAIL

资讯详情

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

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

ECharts visualMap 视觉映射:连续型与分段型颜色映射实战 一张全国门店分布的地图丢到评审会上最常被问的一句话就是哪个省卖得好。我第一反应是给每个省单独调色,手写七八种颜色塞进itemStyle,数据一改就得重来一遍。后来换成Echarts的visualMap组件,才发现这块一直被低估——它不是又一个图例,而是把数据值翻译成视觉通道(颜色、透明度、点大小)的那层翻译器。这篇文章就围绕visualMap的连续型、分段型两条主线,把min/max、inRange/outOfRange、dimension、seriesIndex、pieces、selectedMode、hoverLink这些参数逐个拆开,代码块里逐行加注释,顺手把我踩过的坑一并写清楚。适合刚上手Echarts做数据可视化的同学,也适合做大屏、做地图、做气泡图时被颜色映射折磨过的老手。1. 先给 visualMap 找准位置它是数据到视觉通道的翻译器1.1 从手动调色到声明式映射的思维切换早几年做可视化,我的习惯是给每个数据项单独指定颜色:北京红色、上海蓝色、广东绿色。数据量小还行,一旦省份换成三百个城市,或者销售额每天变,这套写法立刻崩掉。问题不在于代码难写,而在于颜色和数据之间没有任何逻辑关系,读者看到的红到底是高还是危险,全靠我在旁边口头解释。visualMap换了个思路:我不再告诉图表这一项是什么颜色,而是告诉它值从 0 到 1000,颜色从浅蓝到深蓝,你自己算。这层声明式映射带来三个直接好处。第一,颜色和数据强度绑定,视觉上天然可读;第二,颜色规则只有一份,数据更新时不用改;第三,visualMap组件会自动在旁边渲染一条色带或一组分段图例,读者能自己对着看,不用我解释。所以理解visualMap的第一件事,是把它跟legend彻底分开。legend管的是哪些系列/数据项被显示出来,是一种筛选器,它的每一项是离散的、有名字的类目;visualMap管的是某个数值落在什么区间时画成什么样,是一种函数映射,它的每一项是连续的或分段的数值范围。你在散点图上用legend控制显示 A 类还是 B 类,用visualMap控制销售额高的点更红,两者不冲突,但职责绝不能混。1.2 visualMap 与 legend、itemStyle 三者的优先级关系真正让人翻车的是优先级。visualMap算出来的颜色,会写进数据的视觉结果里,但它不是最终颜色。ECharts 内部对样式的处理顺序,大体是数据项的itemStyle 系列级itemStylevisualMap映射结果 主题默认值。这条链子意味着:只要你在series.itemStyle.color里写死了一个颜色,或者某一条数据里带了itemStyle.color,那么visualMap对这个系列(或这条数据)的颜色映射就基本失效了。这个设计本身没问题——它给了你局部覆盖全局的能力,比如某几个重点省份想固定成金色高亮,就可以在数据项里单独写itemStyle。但坑在于:很多人是在别的需求里顺手写了series.itemStyle.color,后来又加visualMap,然后发现组件显示了、色带也在、但地图就是不变色,找半天找不到原因。我的习惯是,只要这个系列打算交给visualMap管颜色,就坚决不在series和data里出现任何color字段,把控制权完整让出来。还有一个容易被忽略的角色是color这个顶层配置。option.color是给没有明确颜色的系列轮换用的调色盘,它跟visualMap不是一层东西,不会覆盖visualMap的结果,但两者颜色撞在一起时视觉上会很乱。大屏项目里我一般会把option.color显式收窄成中性色,避免它跟映射色打架。1.3 两种类型的选择逻辑visualMap有两个type:continuous(连续型)和piecewise(分段型)。选哪个,取决于你想让读者读出什么信息。连续型适合看趋势、看强弱、看密度。全国销售额地图、热力图、气泡图,读者的诉求是哪个更热,一条渐变带足够。分段型适合看归类、看达标线、看等级。比如门店评级 A/B/C/D、用户分层、风险等级,这时候连续渐变反而会让读者纠结这个省到底是甲级还是乙级,切成离散段就明确了。对比维度continuous(连续型)piecewise(分段型)视觉表现平滑渐变色带若干离散色块读者心智强弱、趋势、密度等级、达标、归类关键配置min/max/calculablepieces/selectedMode交互能力可拖拽收窄区间可点击筛选分段典型场景地图、热力图、气泡图评级、分层、阈值告警精度控制precision控制刻度小数label手动写死文本实际项目里这两者不是二选一。我做过一个运营看板,主体用连续型色带展示销售额强弱,右下角再挂一个visualMap数组里的分段型,用selectedMode: single做只看高潜区的快速筛选——两个组件各管一个seriesIndex或不同dimension,互不干扰。2. 连续型 visualMap把 min/max 和插值规则讲透2.1 一份可以直接跑的最小配置逐行注释下面这段是我在实际大屏项目里用的结构,直接贴进setOption就能跑,注释写在每行旁边。option { // visualMap 允许写成对象(表示只有一个),也可以写成数组 // 我习惯一律写数组,后面要加第二个映射规则时不用改结构 visualMap: [ { type: continuous, // 连续型:值到颜色是一条平滑渐变带 min: 0, // 映射区间的下限,注意它是映射下限,不是数据下限 max: 1000, // 映射区间的上限 precision: 0, // 刻度显示的小数位,销售额这种整数就写 0 calculable: true, // 显示可拖拽手柄,拖动可临时收窄映射区间 realtime: true, // 拖拽过程中是否实时重绘,大数据量建议关 inverse: false, // false 值越大越靠 color 数组的尾部 itemWidth: 18, // 色带宽度(垂直排布时就是条的宽度) itemHeight: 160, // 色带长度 align: auto, // 手柄相对色带的位置,auto 让它自己选 text: [高, 低], // 顺序不能反:第一项对应 max,第二项对应 min textGap: 8, // 文字与色带的间距 showLabel: true, // 是否显示两端的刻度数值 orient: vertical, // 垂直排布,适配大屏右侧留白 right: 24, top: middle, // inRange:落在 [min, max] 区间内的值怎么画 inRange: { // 两个以上颜色之间会做线性插值;只写一个颜色就没有渐变可言 color: [#e8f4ff, #7fb8e8, #2b6cb0, #12365c], // 顺手把不透明度也绑上去,做颜色透明度双重编码 opacity: [0.75, 1] }, // outOfRange:落在 [min, max] 之外的值怎么画 outOfRange: { color: #d9d9d9, // 超出区间的数据统一画成灰,一眼就能看出爆表了 opacity: 0.6 }, // 鼠标悬浮在地图某个省上时,色带对应位置会高亮,默认就是开的 hoverLink: true } ], series: [ { type: map, map: china, roam: true, // 关键:这里千万不要再写 itemStyle.color,否则颜色映射会被覆盖 data: [ { name: 广东, value: 980 }, { name: 江苏, value: 760 }, { name: 浙江, value: 690 } ] } ] };这段配置里有三个地方最容易写错:一是text的数组顺序,它跟你想的从左到右没关系,固定是高值在前、低值在后;二是min/max跟数据实际范围的关系,下面单独说;三是inRange.color只给一个颜色,结果就是全图一个色,很多visualMap 不生效的疑问其实是这样来的。2.2 calculable、realtime、precision、inverse 到底改了啥calculable: true会在色带上出现两个可以拖动的圆点手柄。很多人以为它只是个装饰,其实它带来的是一个运行时过滤能力:运营同学拖动手柄,可以把 0-1000 的映射压缩到 400-800,这时候低于 400 的数据会自动落进outOfRange,配合上面那句color: #d9d9d9,整张地图立刻变成只看腰部以上的视图。这比改代码重新发版快得多,做内部看板我很推荐开。realtime是calculable的搭档,默认true,意思是拖动的每一帧都重绘图表。散点图三百个点没问题,但如果你的散点是五万个点加symbolSize映射,拖动时会明显卡顿。这时候把realtime设成false,手柄拖动时只跟手、松手才真正重绘,手感会从黏糊糊变成跟手但有延迟,整体体验反而更好。这个细节官方文档写了但很容易被跳过,我是被现场演示卡住之后才记住的。precision默认是自动推断的——它会去看series.data里该维度的实际精度来定小数位。如果数据里有12.35这种值,刻度可能显示成12.35。做金额、百分比的时候我一般会显式写死,金额写0,百分比写1,避免出现33.3333%这种刻度。inverse: false表示值越大,颜色越靠color数组的尾部。反过来写true,数值大的反而用数组第一个颜色。什么时候需要inverse?比如你做的是故障率或者退货率,业务上希望低值用绿色(数组尾部),高值用红色(数组头部),那color: [#d9480f, #f0b429, #2f9e44]加上inverse: true就能一次写完。不过我更喜欢直接把数组写成[#2f9e44, #f0b429, #d9480f],语义更直观,inverse留给同一套色带要复用两种语义的场景。2.3 inRange 的颜色插值规则与常用通道inRange里能写的通道不止color。按我的使用频率排一下:color:最常用,支持数组插值,也支持单个颜色(无渐变)。opacity:做颜色透明度双重编码,适合深色大屏,能在不换色系的情况下拉开强弱层次。symbolSize:散点图气泡大小的映射,通常跟color搭配做数值规模两个维度的表达。colorAlpha、colorLightness、colorSaturation、colorHue:这四个是 HSL 通道级别的微调,用得少但很管用。比如你有一张品牌色是蓝色的图,想让高值偏暖,可以直接写colorHue: [210, 20],色相从蓝往橙走,不用手动挑一堆颜色。symbol:散点图上按区间换图形(圆点/方块/三角),属于比较花哨的玩法。颜色插值的规则值得单独讲一句:color: [#e8f4ff, #7fb8e8, #2b6cb0, #12365c]这四个颜色不是各占 25%,而是每个颜色平均分布在这条色带的节点上,然后相邻两色之间做线性插值。上面四个颜色其实是在 0%、33.3%、66.7%、100% 四个位置,颜色均匀分布意味着中间那个色被用得更多。如果你希望某个颜色只在很小的区间出现——比如只有超过 900 分才变红——那把它放在数组末尾并不能解决问题,因为末尾的颜色占据最后一整段。这种阈值式跳变的需求,应该用分段型visualMap或者pieces来做,别硬拗连续型。outOfRange的默认行为是把超出区间的值映射到区间边界色,也就是超过 1000 的也按 1000 画。这在很多场景下是错的——你希望一眼看出这个值爆表了。所以我基本都会显式写outOfRange,最常见的写法是给一个中性灰#d9d9d9或者colorAlpha: 0.15,让超限数据在视觉上退到背景里。注意outOfRange只在数据真的超出min/max时才起作用,如果你压根没写min/max,ECharts 会自动取数据的最小最大值作为区间边界,此时永远不会有值落在区间外,outOfRange自然也永远不会触发。这条因果关系是排查为什么我的灰色没生效的关键。3. 分段型 visualMappieces 的边界写法与交互模式3.1 pieces 的六种边界字段到底怎么选piecewise的核心是pieces数组,每一项描述一个数据段。它支持的边界字段有六个:min、max、lt(小于)、gt(大于)、lte(小于等于)、gte(大于等于)。min/max是闭区间,也就是{min: 500, max: 800}会把 500 和 800 都算进来。这在写相邻两段时就有麻烦了:{min: 200, max: 500}和{min: 500, max: 800},那个恰好等于 500 的值到底归谁?这种边界重叠是我最不喜欢的坑,因为数据里恰好出现整百整千的值太常见了。我的做法是相邻分段一律用gtelt组合,把开闭关系写死:visualMap: { type: piecewise, // 写了 pieces 之后,splitNumber 就失效了,以 pieces 为准 pieces: [ // 用 gte lt 把边界卡死,不会出现相邻区间抢同一个值的情况 { gte: 800, label: 800 以上, color: #12365c }, { gte: 500, lt: 800, label: 500 - 800, color: #2b6cb0 }, { gte: 200, lt: 500, label: 200 - 500, color: #7fb8e8 }, // 最后一段不写 gte,把所有更小的值兜住 { lt: 200, label: 200 以下, color: #e8f4ff } ], selectedMode: single, // 一次只选中一段,配合地图做快速筛选 // 初始选中状态:键可以用分段索引,也可以用分段的 value // 这里把索引 3 也就是200 以下默认关掉 selected: { 3: false }, itemWidth: 20, itemHeight: 14, itemGap: 10, itemSymbol: roundRect, // 每段前面那个小色块的形状 textStyle: { color: #cbd5e1 }, right: 24, bottom: 40 }注意最后一段我只写了lt: 200,不写gte,这样所有更小的值(包括负数)都会被它兜住,不会出现数据里有值但没有任何分段匹配的空白。这是写pieces的通用原则:头尾各留一个开放段,中间段严格左右闭合。3.2 label、value、color 在分段里各管什么pieces每一项可以带三个容易混淆的字段:label、value、color。label是显示在分段图例上的文字,不写的话 ECharts 会自动根据边界生成,比如500 - 800。自动生成在大部分时候够用,但有几个场景必须手写:一是单位,自动生成不会带上万元人次;二是业务话术,自动生成的800 以上不如达标或者优秀直观;三是数值本身很长,自动生成会挤成一行,不如自己写成两段。value有两个用途。第一是打包分段——如果你想让几个不连续的区间合成一段显示(比如300-400 和 700-800 都算重点关注),可以在同一个 piece 里写{ value: 100, label: 重点关注, color: #f0b429, ... }这样多个分段的集合值。第二是用在selectedMode的选中状态里,selected的键可以写value而不是索引,这样往pieces中间插一段也不会打乱已有的初始选中状态。这是个小技巧,但在会反复调整分段规则的迭代期特别省事。color就是这一段对应的颜色。这里有个细节:分段型的color写在pieces每一项上,和连续型写在inRange.color里不是一个位置。第一次从连续型切到分段型的人,经常会习惯性地去找inRange,结果发现怎么都不生效。记住:piecewise的颜色要么写在每个piece里,要么写在inRange.color数组里按顺序对应(数组长度跟分段数一致),后者我一般不用,因为一旦调整分段数量,数组顺序就容易错位。3.3 selectedMode 与 selected把 visualMap 当筛选器用selectedMode默认是multiple,也就是每段前面带个小方框,点一下切换这一段的显示,可以同时选中多段,相当于只看这几个等级。设成single就变成单选,点哪段就只看哪段,其他段的数据会被隐藏(不是变灰,是不画)。这个能力用好了很值。我做过一个门店等级看板,地图上 300 个城市按等级分成 A/B/C/D 四段,selectedMode: single,老板点一下A 级,地图上只剩 A 级城市,再点一下D 级,立刻看到需要帮扶的区域。整套交互零额外代码,比另外做一套筛选控件轻得多。配合selected可以做初始状态。这里要注意两个细节:一是键的写法,索引从 0 开始;二是被取消选中的分段,对应的数据在图表上直接不渲染,而不是落到outOfRange。如果你的需求是高亮重点段,其余淡化,那selectedMode就不合适,应该用pieces里的color加上outOfRange来做,而不是靠选中状态。还有一个坑:selectedMode设为multiple时,如果所有段都被取消选中,图表会变成空白。用户可能会以为图表坏了。保险做法是在selected里至少保证一段默认选中,或者干脆在页面上加一句提示文案。这个坑不大,但出现在客户面前就很尴尬。4. dimension 与 seriesIndex多维度数据里最容易翻车的地方4.1 默认维度推断规则与三种 data 结构对照dimension是visualMap里我见过最多人踩坑的参数,因为它的默认推断规则是隐式的:不写dimension时,visualMap会取数据项里最后一个维度作为映射依据。这个规则对简单数据是够用的,但一旦数据的结构变复杂,就会悄悄映射到你没预期的那个值上。下面这张表是我梳理的三种常见data结构对应关系:series.data 结构默认映射到想映射别的怎么办[120, 200, 150]数组值本身不需要,本身就是一维[{name:北京, value: 320}]value不需要,默认就取 value[[116.4, 39.9, 820, 12]]索引 3(最后一维)写dimension: 2映射到 820[{value:[116.4,39.9,820]}]索引 2写dimension: 2datasetencode取最后一个维度dimension可以写维度名字符串最后一行是我觉得最优雅的用法。当使用dataset管理数据、并且在series里配了encode之后,visualMap.dimension可以直接写数据集里的字段名,比如dimension: sales,而不是写一个容易记错的数字索引。这样列顺序调整时,映射关系不会跟着错位。回到实测:那些颜色全是一个色的 bug,九成以上是dimension取到的值恰好是常量或者空。典型例子是散点图data: [[lng, lat, value]],不带dimension时取到的是最后一维,运气好正好是value;但如果你的数据是[[lng, lat, name, value]]这种混搭结构,最后一维可能是个字符串,visualMap拿到字符串算不出比例,所有点就都用了同一个颜色。排查方法很简单:在控制台把series.data打出来,看最后一项到底是什么类型。4.2 一个 visualMap 管全部还是各系列配一个visualMap.seriesIndex用来指定这个映射规则作用于哪些系列,不写就是全部。默认全部在小项目里很方便,但在多系列图表里会造成意外:你可能只想让折线图的点按数值渐变,结果柱状图、辅助线系列也跟着被染色了。我的分界标准是这样的:如果多个系列的数值量纲一致(比如都是销售额,单位相同),那就用一个visualMap管全部,这样读者看一个色带就能理解所有系列,认知成本最低。如果量纲不一致(一个是销量,一个是增长率),那必须各配一个,分别写seriesIndex,否则颜色就失去了统一语义——读者看到深红,不知道是销量高还是增长率高,颜色反而成了噪音。多visualMap时还有一个必须注意的原则:不要有两个 visualMap 抢同一个视觉通道。比如两个都映射color并且seriesIndex有重叠,后配置的会盖掉前面的,表现是其中一个 visualMap 好像完全不起作用。这种情况要么改seriesIndex让它们作用范围不重叠,要么让它们分别映射到不同通道(一个color,一个symbolSize)。4.3 用两个 visualMap 做气泡图的双重编码气泡图是dimension最能发挥价值的地方。假设数据每项是[经度, 纬度, 销量, 门店数],我想让颜色表示销量、大小表示门店数,那就需要两个visualMap:series: [{ type: scatter, coordinateSystem: geo, // 每项四列:经度、纬度、销量、门店数 data: [ [116.40, 39.90, 820, 12], [121.47, 31.23, 610, 9], [113.26, 23.13, 940, 15] ] }], visualMap: [ { type: continuous, seriesIndex: 0, // 只作用于第 0 个系列 dimension: 2, // 用第 3 列(销量)映射颜色 min: 0, max: 1000, inRange: { color: [#fff5e6, #ffb84d, #d9480f] }, right: 24, top: 40 }, { type: continuous, seriesIndex: 0, dimension: 3, // 用第 4 列(门店数)映射气泡大小 min: 0, max: 20, // 这里绝对不要再写 color,否则会跟上面那个 visualMap 抢颜色通道 inRange: { symbolSize: [8, 42] }, right: 110, // 两个色带错开位置,避免重叠 top: 40 } ]两个visualMap各占一个通道,读者看左侧色带读销量,看右侧读规模,信息密度翻倍但认知负担没增加多少。不过我有个实测心得:symbolSize的最大值不要给太夸张。我一开始写[10, 80],结果几个大城市的气泡直径 80 像素,把周边的小点全糊住了。后来改成[8, 42],并且把大面积气泡的opacity降到 0.85,重叠部分还能看出层次。散点越密,气泡最大值就该越小,这是个需要实测调的经验值,没有公式。5. 那些不报错但结果不对的坑覆盖、联动与取色5.1 颜色不生效一条完整的排查链路这个问题的排查我走过完整的弯路,分享下我现在的检查顺序,基本能覆盖九成情况。第一步:先确认色带组件本身有没有出来。如果visualMap组件连色带都没渲染,那是配置结构的问题——大概率是type写错了,或者把它放进了series里面(它必须挂在option顶层)。第二步:色带出来了但地图不变色,查itemStyle。去series和series.data里搜color关键字。只要搜到,基本就是它。这里有个隐蔽情况:很多主题或者基础模板会在series里写itemStyle: { color: ... },你复制模板的时候没注意,后面加visualMap就永远不生效。第三步:还是没有,查dimension和数据类型。把series.data打印出来,确认visualMap实际取到的那一维是数字。如果某一项的值是null、undefined或者字符串,那一项会走outOfRange或者使用默认颜色。第四步:查min/max是不是写反了或者范围过窄。我遇到过一次min: 1000, max: 0的写法,当时是复制粘贴改数字改漏了,结果所有值都落在区间外,全图走outOfRange的灰色。这种错误 ECharts 不会报错,只是静静地画一片灰。第五步:用visualMap.show false做个对照实验。把组件隐藏掉,如果颜色映射还是生效的,说明映射逻辑没问题,问题出在组件的显示配置上;如果隐藏之后也不生效,那问题一定在映射规则本身。这个二分法能快速缩小范围。5.2 地图全是一个色dimension 取错与 outOfRange 的组合问题地图场景有自己特有的坑。当series.type是map时,data结构是[{name: 北京, value: 320}],这个结构下默认取value,一般不会错。但有两个变体容易出问题。一是用经纬度做地图散点。series-map配geo做散点叠加时,data变成了[{name:北京, value: [116.4, 39.9, 320]}],这时候默认取的是value这一整个数组,而不是数组里的第三项。表现就是所有点同一个颜色。必须显式写dimension: 2。二是数据里存在null的省份。地图上有些省份没有数据,value会被当成NaN。这时候它们会走outOfRange。如果你的outOfRange忘了配,默认可能是不渲染(区域留白)或者用默认色,视觉上会让人以为这个省数据是 0。我的做法是显式给outOfRange配一个浅灰,并且在图例旁边标注灰色表示暂无数据,避免误读。顺带说一句visualMap和dataZoom的关系。dataZoom缩放的是坐标轴范围,但visualMap的映射区间不会自动跟着dataZoom重算。也就是说你把折线图放大到只看后三个月,颜色仍然是按照全量数据的min/max来算的,放大区域的颜色可能整体偏浅。这是设计使然,不是 bug。如果确实需要跟着视口变,得监听dataZoom事件,手动setOption更新min/max,而且要注意处理节流——dataZoom触发非常频繁,直接在里面setOption会明显掉帧。5.3 tooltip 里拿不到映射后的颜色怎么补这是个挺实际的问题:tooltip默认的marker小圆点用的是系列颜色,而不是visualMap算出来的那一项的颜色。所以你会看到地图上这个省深蓝、tooltip 里的小点却是系列默认色,对不上。visualMap没有提供给我算一下某个值的颜色的公开 API,所以只能在formatter里自己实现一遍插值。因为插值逻辑很简单(就是线性),自己写反而更可控:// 与 visualMap.inRange.color 保持一致的取色函数 // stops 就是 inRange.color 那个数组,顺序必须完全一致 function pickColor(stops, value, min, max) { if (stops.length 1) return stops[0]; // 只有一个颜色就没得插 // 先把值归一到 0~1,超过范围的直接夹到边界 const t Math.min(1, Math.max(0, (value - min) / (max - min))); const pos t * (stops.length - 1); // 落在色带的哪个位置 const i Math.min(stops.length - 2, Math.floor(pos)); // 左端点索引 const local pos - i; // 段内比例 0~1 const hex2rgb (h) { const s h.replace(#, ); return [0, 2, 4].map((k) parseInt(s.slice(k, k 2), 16)); }; const a hex2rgb(stops[i]); const b hex2rgb(stops[i 1]); // 逐通道线性插值,ECharts 的默认行为也是在 RGB 空间做的 const rgb a.map((v, k) Math.round(v (b[k] - v) * local)); return rgb(${rgb.join(,)}); } // 在 tooltip.formatter 里用 tooltip: { formatter: (p) { const color pickColor( [#e8f4ff, #7fb8e8, #2b6cb0, #12365c], p.value, 0, 1000 ); return span styledisplay:inline-block;width:8px;height:8px; border-radius:50%;background:${color};margin-right:6px/span ${p.name}: ${p.value}; } }这里提醒一点:这个函数要跟visualMap.inRange.color保持同步。如果inRange里还用了colorHue或者colorLightness这类 HSL 通道,手写插值就对不上了,那种情况我会宁可放弃 tooltip 的精确配色,改用统一的中性色小点,不给自己埋同步维护的债。6. 大屏与大数据场景下的调优性能、取色与可读性6.1 拖拽卡顿的三种降级手段calculable在演示时很加分,但数据量一大就变成性能杀手,因为手柄拖动的每一帧都会触发全量重新计算视觉映射并重绘。我按实际效果从轻到重排了三种降级方案。最轻的是realtime: false,只改这一个参数,拖动时图表不动、松手才更新。视觉上有轻微延迟感,但帧率稳住了,推荐作为第一选择。第二档是限制参与映射的数据量。如果图表本身是五万个点的散点,与其优化visualMap,不如先用sampling或者预聚合把点降到几千个。visualMap的计算量跟点数成正比,降采样是最直接的收益。第三档是放弃calculable,改成外部控件。把calculable: false,在旁边放两个滑块或者几个预设按钮(比如全部/TOP 50%/TOP 20%),点击时直接setOption改min/max。这样做的好处是每次交互只重绘一次,而且预设按钮比手动拖拽更容易复现,内部看板我一直用这个方案。6.2 深色大屏的配色插值与对比度大屏几乎都是深色背景,visualMap的颜色需要重新挑一遍。浅色背景上那套浅色→深色的渐变放到深色背景上,深色端会直接糊进背景里,彻底看不见。我现在的套路是反着来:深色端不用纯深色,而是用高饱和的中亮色。比如浅底用[#dbeafe, #3b82f6, #1e3a8a],深底就换成[#1e3a8a, #3b82f6, #93c5fd]——低值用深蓝融进背景,高值用亮蓝跳出来。关键判断标准是:把色带截图丢到深色底上,眯起眼睛看,如果低值端和背景完全分不清,那这段色带在低值密集的区域就等于没画。另外深色大屏我很喜欢用opacity做第二编码。地图上低值区域用opacity: 0.35,高值区域opacity: 1,配合色相变化,远近层次一目了然。这招在投屏、投影仪这类对比度不稳定的设备上尤其管用,因为透明度变化比色相变化更抗色偏。6.3 色盲友好与颜色形状双重编码纯靠颜色编码强弱有个先天问题:红绿色盲人群看渐变带是分不出层次的。我做对外发布的图表时,习惯加一层非颜色的编码。最容易实现的是symbolSize——散点图的点本身就带大小,那就让大小也跟着同一个维度走:visualMap: { type: continuous, dimension: 2, min: 0, max: 1000, inRange: { color: [#e8f4ff, #2b6cb0], // 颜色编码 symbolSize: [6, 26] // 大小也编码同一个维度,做冗余 } }这样即便完全看不出颜色差异,光看点的大小也能读出强弱。地图类图表没法定点大小,退路是给outOfRange和数据缺失的区域配明确不同的填充花纹或者灰度,让有数据/无数据不依赖颜色区分。另外我个人经验是,两色渐变比多色渐变更安全——[#e8f4ff, #12365c]这种从浅到深的单一色相渐变,绝大多数色觉类型都能分辨,而三色以上的彩虹色带反而是最容易出问题的。工具箱里还有一件事值得做:如果图表要对外发布,给visualMap配上清晰的text标签和单位,不要让读者去猜色带两端代表什么。我见过太多大屏,色带画得很漂亮,但两头没有文字,读者只能凭感觉估。一个text: [高(万元), 低(万元)]就能省掉一次解释。7. 组合场景visualMap 在折线柱状图上的用法与限制7.1 用柱状图染色做阈值告警visualMap不只服务于地图和散点,直角坐标系上的柱状图、折线图同样能用。最常见的用法是给柱状图按数值染色,让超过阈值的那几根柱子自然变红:visualMap: { type: piecewise, // 直角坐标系的柱状图,默认按 y 值映射,不用写 dimension pieces: [ { gte: 90, label: 达标(90 分以上), color: #2f9e44 }, { gte: 60, lt: 90, label: 预警(60-90 分), color: #f0b429 }, { lt: 60, label: 告警(60 分以下), color: #d9480f } ], // 这里强烈建议放在图表外部,别压在柱子上 orient: horizontal, left: center, bottom: 0, itemWidth: 14, itemHeight: 14 }这里有个实际踩过的坑:visualMap默认会占用图表内部的空间,如果grid没给它留位置,色带就会压在柱子或者 x 轴上。所以直角坐标系用visualMap的时候,一定要检查grid的bottom/right留了多少空间。我的习惯是给grid.bottom至少留 60 像素,orient: horizontal放在底部,视觉上跟 x 轴的标签错开。另一个观察是:柱状图上用visualMap跟用itemStyle.color回调函数,效果上差不多,但后者更灵活(可以按索引、按业务规则任意上色),前者更声明式(颜色跟数值强绑定,还有图例)。如果颜色规则本身就是按数值分段,我用visualMap;如果是按业务标签上色(比如按产品线上色),那就别硬套visualMap,老老实实用itemStyle回调,把visualMap留给真正需要读数值的场景。7.2 折线图上用颜色映射一个常见误解经常有人问能不能让折线的颜色随着数值变化,比如一根折线在某段变红、某段变绿。这里要说清楚:visualMap映射的是数据点的视觉属性,对于折线来说,能控制的是折线上数据点(symbol)的颜色和大小,而线段本身的颜色是系列级属性,不会因为数值变化而分段变色。如果你真的需要折线分段变色,标准做法是用visualMap控制symbol的color和symbolSize(把showSymbol打开、symbolSize调大一点),视觉上靠点的颜色来体现数值区间,线段保持中性色。这样既保留了一根线的连续性,又能看出数值波动。硬要去改线段颜色的话,得用markArea或者拆成多个系列来做,复杂度和收益完全不成比例。7.3 多个 visualMap 堆叠时的布局协调回到大屏场景。一个页面上同时出现三个visualMap组件是很常见的(一个地图的色带、一个气泡图的大小图例、一个分段的等级筛选)。这时候布局就成了主要矛盾。我的通用做法是给每个visualMap显式指定left/right/top/bottom中的两个,而不是依赖align: auto。理由很简单:auto是根据色带的排布方向自动选择手柄位置,它不解决多个组件之间怎么不重叠的问题。我通常会把连续型的色带统一放在右侧、垂直排布,right依次取 24、110、196 这样错开,分段型的放在底部水平排布。这样无论页面上有几个映射组件,它们各自的位置都是确定的、可预期的。一个细节:orient: vertical的色带,itemHeight决定的是它的视觉高度,而这个高度不会自动跟随容器缩放。做响应式大屏的时候,我会监听窗口resize事件,按屏幕高度比例重新计算itemHeight和字号,再setOption一次。这个逻辑不复杂,但如果不做,在 4K 屏上色带会显得特别短小,在笔记本上又可能超出容器。8. 我日常使用 visualMap 的一些固定习惯8.1 我固定会写的几个参数用久了之后,我发现有这么几个参数我会在新项目里无脑先写上,再按需调整。precision: 0是第一个。默认的自动推断在小数位多的时候会让刻度长得很难看,显式写死一位或零位,刻度立刻清爽。text: [高, 低]是第二个。哪怕只是占位,也比两头空白强,至少提醒读者色带的方向。等需求明确了再换成带单位的文案。outOfRange是第三个。我默认会写一个浅灰,因为数据超限和数据缺失在业务上都是需要被看见的信息,不能让它们自动映射到边界色被掩盖掉。hoverLink: true是第四个。它默认就是开的,但我会显式写出来,因为它在某些复杂布局下会被误关,显式声明能避免排查时怀疑到它头上。8.2 数据里出现 0 和 null 时的处理业务数据里 0 和空值是完全不同的意思:0 是确实没有销量,空值是这台设备没上报。但在visualMap眼里,0 是一个合法值,会被正常映射到色带的起点;null会被当成缺失,通常走默认渲染。如果两者在视觉上混在一起,读者就没法区分。我的处理方式是:把缺失值在数据层就替换成一个显式的哨兵值(比如负数),然后把这个负数区间交给outOfRange画成灰色带斜纹(分段型)或者固定灰(连续型),同时在text或者页面说明里注明灰色表示无数据。这个做法需要在数据预处理阶段做一次转换,多写几行代码,但能彻底消除0 和空值混淆这类误读。8.3 关于该不该显示 visualMap 组件的判断最后一个经验:visualMap的映射能力和组件显示是两回事。用show: false可以把组件隐藏起来,但颜色映射照常生效。什么时候该隐藏?当色带的语义已经被其他元素表达清楚的时候。比如一张只有红蓝两色、图例里已经标明了红超标、蓝正常的达标率图,再挂一个色带是冗余的,反而占地方。这时候用show: false保留映射逻辑、去掉视觉噪音,是最干净的做法。什么时候绝对不能隐藏?当你的色带是多色渐变、读者无法凭直觉判断方向的时候。多色渐变如果不给图例,读者看到橙红和深蓝根本不知道哪个代表高值。这种情况下宁可挤一点位置,也要把组件显示出来。我判断的标准很简单:把图截图给一个不了解业务的人看,他能不能说出颜色越X,数值越Y。说不出来,组件就必须显示。8.4 一个常用的调试小技巧最后分享一个排查visualMap问题时我经常用的方法:把inRange.color临时换成[#000000, #ffffff]这种黑白两色,outOfRange换成明亮的品红#ff00ff。黑白渐变能让你一眼看出映射的方向对不对、梯度是否平滑、有没有出现意料之外的整块同色区域。而品红色是个在真实配色方案里几乎不会出现的颜色,只要图上出现了品红,你立刻就知道有多少数据落在区间外、分布在哪些位置。这个技巧在排查数据是不是超出了 min/max的时候,比在控制台里一条条数数据快得多。等排查完,把颜色换回来就行。这套流程走下来,visualMap基本就不再是那个配置了很多参数但不知道哪个在起作用的黑盒了。真正需要花心思的其实不是参数本身,而是先想清楚你要让读者从颜色里读出什么——是想看强弱分布,还是想看等级归类,还是想框出一个筛选区间。想清楚这一层,type、min/max、pieces、dimension这些配置就是顺着逻辑自然写下来的,不会有纠结。
返回列表