添加 useRelation 工具函数
添加 useRelation 工具函数
useRelation 主要用于增加所有比例尺的条件映射能力。条件映射能力就是在输入满足指定条件的时候返回期望的值,在不满足条件的情况下走比例尺的默认逻辑。比如在下面的热力图的例子中,需要当值为 NaN 的时候为灰色,在值为 0 的时候为白色,其他的时候就走比例尺的默认映射逻辑。也可以看这个 https://github.com/antvis/G2Plot/issues/3329 里面提到的问题。

使用方式
- 在 scale 中的使用方式
import { Sequential, useRelation } from '@antv/scale';
const scale = new Sequential({
domain: [0, 100],
interpolator: d3.interpolatePuRd,
unknown: 'Opps',
});
const relations = [
[Number.isNaN, '#eee'], // 函数 relation,验证函数返回 true 的时候才返回对应值
[0, '#fff'], // 值 relation,当输入和值相等的时候就返回对应值
];
const [conditionalize, deconditionalize] = useRelation(relations);
// 条件化
conditionalize(scale);
scale.map(NaN); // '#eee'
scale.map(0); // '#fff'
// 去条件化,恢复默认映射逻辑
deconditionalize(scale);
scale.map(NaN); // 'Opps';
scale.map(0); // d3.interpolatePuRd(0)
- 在 G2 5.0 的使用方式
const options = {
scale: {
color: {
type:'sequential',
relations: [
[Number.isNaN, '#eee'],
[0, '#fff'],
],
},
},
};
设计思考
- 为什么不在基类上增加对应的能力,而是动态修改实例?
这主要性能上的考量,希望在没有 relations 的情况下,每次 map 不需要去做一次额外的判断:判断是否需要走 relations 的逻辑。所以这要求 map 必须是根据 relations 动态生成的:没有 relations 的情况就直接使用原始的 map,否者使用条件化之后的 map。
// 不希望出现如下的代码
class Base {
map(x) {
if (isInRelation(x)) {
} else return this._innerMap(x);
}
}
- 为什么作为 @antvis/scale 的内置函数?
因为这个能力挺常用的,不一定只会在 G2 内部使用,任何使用 @antvis/scale 的库都可能有相同的需求。
- 为什么用一个数组来描述 relations?
- 不使用 Object:因为 Object 的 key 只能是字符串,但是这个条件可以是一个验证函数,也可以是一个具体的值。
- 不使用 Map:如果 key 是一个验证函数,就不能通过
Map.get(x)API 来获得映射之后的值,就失去了 Map 的意义。
实现建议
- 函数 relation 的优先级比值 relation 高。
- 将 relations 分为两类,函数 relation 走函数映射,值 relation 通过生成一个 Map 来映射。
- 目前每一个 conditionalize 只用能对一个 scale 使用就好,不需要对多个 scale 使用。
function useRelation(relations) {
let map = null;
let invert = null;
const conditionalize = (scale) => {
if (relations.length == 0) return;
// 保留原始的方法
map = scale.map.bind(scale);
invert = scale.invert?.bind(scale);
// 修改 map 方法
scale.map = function (x) {
if (isInFunctionDomain(x)) {
//...
} else if (isInValueDomain(x)) {
//...
} else return map(x);
};
// 修改 invert 方法
if (!invert) return;
scale.invert = function (x) {
if (isInFunctionRange(x)) {
//...
} else if (isInValueRange(x)) {
//..
} else return invert(x);
};
};
const deconditionalize = (scale) => {
if (map !== null) scale.map = map;
if (invert !== null) scale.invert = invert;
};
return [conditionalize, deconditionalize];
}
~~有一个问题:scale 的方法继承自基类。如果直接这么修改方法,应该是基类方法被修改。这不行吧?~~ 试了下,好像不会,当我没说 🤭。没有直接修改 prototype 的方法,应该就没问题。
~有一个问题:scale 的方法继承自基类。如果直接这么修改方法,应该是基类方法被修改。这不行吧?~ 试了下,好像不会,当我没说 🤭。没有直接修改 prototype 的方法,应该就没问题。
嗯嗯,不修改 prototype 上的方法应该是没有问题的。
如果是 d3-scale 的话这种 monkey patch 是挺合理的,但作为 @antv/scale 的特性在内部修改也成。
如果是单独使用 @antv/scale 的话,确实不太需要使用 relations 这个功能。因为使用者对 domain 和 range 是完全掌握的,所以更好的做法是这个方法直接写在 G2 里面。