scale icon indicating copy to clipboard operation
scale copied to clipboard

添加 useRelation 工具函数

Open pearmini opened this issue 3 years ago • 4 comments

添加 useRelation 工具函数

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

image

使用方式

  • 在 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'],
      ],
    },
  },
};

设计思考

  1. 为什么不在基类上增加对应的能力,而是动态修改实例?

这主要性能上的考量,希望在没有 relations 的情况下,每次 map 不需要去做一次额外的判断:判断是否需要走 relations 的逻辑。所以这要求 map 必须是根据 relations 动态生成的:没有 relations 的情况就直接使用原始的 map,否者使用条件化之后的 map。

// 不希望出现如下的代码
class Base {
  map(x) {
    if (isInRelation(x)) {
    } else return this._innerMap(x);
  }
}
  1. 为什么作为 @antvis/scale 的内置函数?

因为这个能力挺常用的,不一定只会在 G2 内部使用,任何使用 @antvis/scale 的库都可能有相同的需求。

  1. 为什么用一个数组来描述 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];
}

pearmini avatar Sep 01 '22 06:09 pearmini

~~有一个问题:scale 的方法继承自基类。如果直接这么修改方法,应该是基类方法被修改。这不行吧?~~ 试了下,好像不会,当我没说 🤭。没有直接修改 prototype 的方法,应该就没问题。

pepper-nice avatar Sep 01 '22 07:09 pepper-nice

~有一个问题:scale 的方法继承自基类。如果直接这么修改方法,应该是基类方法被修改。这不行吧?~ 试了下,好像不会,当我没说 🤭。没有直接修改 prototype 的方法,应该就没问题。

嗯嗯,不修改 prototype 上的方法应该是没有问题的。

pearmini avatar Sep 01 '22 07:09 pearmini

如果是 d3-scale 的话这种 monkey patch 是挺合理的,但作为 @antv/scale 的特性在内部修改也成。

xiaoiver avatar Sep 01 '22 12:09 xiaoiver

如果是单独使用 @antv/scale 的话,确实不太需要使用 relations 这个功能。因为使用者对 domain 和 range 是完全掌握的,所以更好的做法是这个方法直接写在 G2 里面。

pearmini avatar Sep 02 '22 07:09 pearmini