d3源码之d3-scale

要尝试把之前用d3画的东西放到react-native上, 而rn是没有dom只有svg的lib, 那么就要研究下d3的实现了.

背景&目标

之前用d3做了一个事件时间线, 用到了d3-scale, d3-brush, d3-selection. 那么在rn上无法对dom(其实是svg)进行拿起来干式的操作, 那么想在rn上模仿一个类似的时间线就要去了解一下d3的实现了.

所以决定从最简单的d3-scale开始, 这个lib算是个数学库, 不涉及dom操作, 在时间线的项目中使用到的api也不多, 那么目标就设为了解这几个api的运行流程.

  1. d3.scaleLinear()
  2. .domain().range()
  3. scale()scale.invert()

我也fork了一份代码用来写注释.

目录结构

d3应该是从大而全拆成各个小module再互相引用的, 用了rollup, 从rollup.config看到入口是在根目录下的index.js, 而具体内容在src文件夹下, index.js的内容全是export {xx as xxx} from './src/xx'. 这个目录结构相当简单, 适用于d3的所有小模块的.

scaleLinear

我们的目标是d3.scaleLinear(). 所以来到src/linear.js. 完整版从这里看, 截取export的地方贴一下:

export default function linear() {
  var scale = continuous(deinterpolate, reinterpolate);

  scale.copy = function() { // 给scale添加copy方法
    return copy(scale, linear());
  };

  return linearish(scale); // 给scale添加了些方法并返回scale, 所以核心还是第一句continuous()
}

从这个输出结构看出, 输出的是scale对象(应该是一个方法). 因为这里是linear, 所以对scale做了处理:linearish, 字面意思"线性化", 所以结论是不同的scale的核心是同一个工厂, 再经过不同的包装重载一些方法来输出不同的scale.

那么我们要研究的就是: continuous(deinterpolate, reinterpolate). 这三个变量何去何从都待我们一个个看过来.

deinterpolate, reinterpolate

deinterpolate:

来源: countinous的deinterpolateLinear.

export function deinterpolateLinear(a, b) {
  return (b -= (a = +a)) // b = b - a, 不知道为什么要花里胡哨
      ? function(x) { return (x - a) / b; } // 当a, b不相等, 返回 x 在 a, b中的比例. 也就是 x - a / b - a
      : constant(b); // 当a, b相等 永远返回  0
}

reinterpolate:

来源: 另一个模块: d3-interpolateinterpolateNumber.

export default function(a, b) {
  return a = +a, b -= a, function(t) {
    return a + b * t; // return a + (b - a) * t
  };
}

代码很简单, 注释也写了, d3这个库有个特点就是花里胡哨, 而且各个小模块的花式还不同, 作者应该是在尝试各种招式~ 来总结一下:

这两个函数都接受两个参数, 是实际范围. 这两个函数都是在实际范围和x在范围内的位置做转换. 位置用数字来表示百分比.

deinterpolate返回的函数接收参数x: 实际点, 返回点在范围中的位置, 用0~1来表示.

reinterpolate返回的函数接收参数t: 位置, 返回这个位置对应的点.

后来发现在continuous的代码中有注释:

// deinterpolate(a, b)(x) takes a domain value x in [a,b] and returns the corresponding parameter t in [0,1].
// reinterpolate(a, b)(t) takes a parameter t in [0,1] and returns the corresponding domain value x in [a,b].

continuous

完整版源码在这里. 函数是下面这个样子的, 注释也贴上了:

export default function continuous(deinterpolate, reinterpolate) {
  var domain = unit,
      range = unit,
      interpolate = interpolateValue,
      clamp = false,
      piecewise,
      output,
      input;
  /*
    continuous返回值, 返回值调用任何方法的返回值都是这个.
    对piece wise做了处理, 把output和input置空,
    最后返回scale.
   */
  function rescale() {
    piecewise = Math.min(domain.length, range.length) > 2 ? polymap : bimap; // 我们使用的都是length === 2的, 所以是bimap
    output = input = null;
    return scale;
  }

  function scale(x) {
    return (output || (output = piecewise(domain, range, clamp ? deinterpolateClamp(deinterpolate) : deinterpolate, interpolate)))(+x);
    /*
        翻译:
        1. 输出: piecewise(domain, range, deinterpolate, interpolate)(x)
        2. clamp是通过scale.clamp()设置的, 超出范围是否纠正到范围内, 默认false, 如果是true会小小改写deinterpolate方法
        3. 在调用rescale()前都会保存当前输出(不重新计算, 因为结果肯定是一样的). rescale会在调用scale的任何方法时调用.
     */
  }

  scale.invert = function(y) {
    return (input || (input = piecewise(range, domain, deinterpolateLinear, clamp ? reinterpolateClamp(reinterpolate) : reinterpolate)))(+y);
    /*
        和上面scale一样, 调用了piecewise, 传了不同的参数~ 让我们到bimap里去研究吧.
     */
  };

  scale.domain = function(_) {
    return arguments.length ? (domain = map.call(_, number), rescale()) : domain.slice();
    /*
        如果不传参, 返回当前domain, 阻断链式操作
        如果传了, domain = _.map( a => +a), 然后返回rescale(), 也就是一顿操作再返回scale
     */
  };

  scale.range = function(_) {
    return arguments.length ? (range = slice.call(_), rescale()) : range.slice();
    /*
        和domain一样, 可能range不一定要是数字.
     */
  };

  scale.rangeRound = function(_) {
    return range = slice.call(_), interpolate = interpolateRound, rescale();
  };

  scale.clamp = function(_) { // 设置超出范围是否纠正到范围内
    return arguments.length ? (clamp = !!_, rescale()) : clamp;
  };

  scale.interpolate = function(_) { // 这个本来是从d3-interpolate引入的, 修改这个会改变算法
    return arguments.length ? (interpolate = _, rescale()) : interpolate;
  };

  return rescale();
}

归纳:

  1. continuous()调用返回值是一个方法.
  2. 因为闭包, 所以返回的这个方法里保存了一些属性: domain, range, clamp等.
  3. 返回值是scale(), 就是我们使用的比例尺.
  4. scale()相对的是scale.invert(), 使用的是同一个生成函数.
  5. 每次通过方法改变scale的属性(domain, range, clamp等)就会触发rescale().
  6. rescale()的作用两个: 根据domain和range的维度来改变scale使用的函数; 重置缓存(因为属性不变输出是不变的所以不触发rescale()再次调用scale()不会重新计算).

另外:

  • 我们使用场景domain和range维度都是2, 所以都用了bimap这个方法.
  • 学到一个奇怪的用法: function还可以有自己的键值, 因为 (function () {}) instanceof Object === true吧.

pow

看完以后来看了一下scalePow()是如何实现的.

function raise(x, exponent) {
  return x < 0 ? -Math.pow(-x, exponent) : Math.pow(x, exponent);
}

export default function pow() {
  var exponent = 1,
      scale = continuous(deinterpolate, reinterpolate),
      domain = scale.domain;

  function deinterpolate(a, b) {
    return (b = raise(b, exponent) - (a = raise(a, exponent)))
        ? function(x) { return (raise(x, exponent) - a) / b; }
        : constant(b);
  }

  function reinterpolate(a, b) {
    b = raise(b, exponent) - (a = raise(a, exponent));
    return function(t) { return raise(a + b * t, 1 / exponent); };
  }

  scale.exponent = function(_) {
    return arguments.length ? (exponent = +_, domain(domain())) : exponent;
  };

  scale.copy = function() {
    return copy(scale, pow().exponent(exponent));
  };

  return linearish(scale);
}

同样的返回值是continuous(deinterpolate, reinterpolate).

只是重写了deinterpolatereinterpolate.

总结

d3-scale可以说是教科书式的工厂模式, 一个核心方法, 通过重写参数来提供不同api.

scaleLinear()的过程是:

  1. scaleLinear()返回值是一个带有内部属性的对象, 表面自己就是可以直接调用的方法.
  2. 通过一些方法来设置内部属性. domain和range默认是[0, 1].
  3. 核心算法就是加减乘除的比例尺. 通过domain和range和策略来输出结果.
全部评论

相关推荐

最近又搬回宿舍了,在工位坐不住,写一写秋招起伏不断的心态变化,也算对自己心态的一些思考表演式学习从开始为实习准备的时候就特别焦虑,楼主一开始选择的是cpp后端,但是24届这个方向已经炸了,同时自己又因为本科非92且非科班,所以感到机会更加迷茫。在某天晚上用java写出hello&nbsp;world并失眠一整晚后选择老本行干嵌入式。理想是美好的,现实情况是每天忙但又没有实质性进展,总是在配环境,调工具,顺带还要推科研。而这时候才发现自己一直在表演式学习,徘徊在设想如何展开工作的循环里,导致没有实质性进展。现在看来当时如果把精力专注在动手写而不是两只手端着看教程,基本功或许不会那么差。实习的焦虑5月,楼主...
耶比:哲学上有一个问题,玛丽的房间:玛丽知道眼睛识别色彩的原理知道各种颜色,但是她生活在黑白的房间里,直到有一天玛丽的房门打开了她亲眼看到了颜色,才知道什么是色彩。我现在最大可能的减少对非工作事情的思考,如果有一件事困扰了我, 能解决的我就直接做(去哪里或者和谁吵架等等……),解决不了的我就不想了,每一天都是最年轻的一天,珍惜今天吧
投递比亚迪等公司10个岗位 > 秋招被确诊为…… 牛客创作赏金赛
点赞 评论 收藏
分享
ProMonkey2024:5个oc?厉害! 但是有一个小问题:谁问你了?😡我的意思是,谁在意?我告诉你,根本没人问你,在我们之中0人问了你,我把所有问你的人都请来 party 了,到场人数是0个人,誰问你了?WHO ASKED?谁问汝矣?誰があなたに聞きましたか?누가 물어봤어?我爬上了珠穆朗玛峰也没找到谁问你了,我刚刚潜入了世界上最大的射电望远镜也没开到那个问你的人的盒,在找到谁问你之前我连癌症的解药都发明了出来,我开了最大距离渲染也没找到谁问你了我活在这个被辐射蹂躏了多年的破碎世界的坟墓里目睹全球核战争把人类文明毁灭也没见到谁问你了(别的帖子偷来的,现学现卖😋)
点赞 评论 收藏
分享
头像
11-09 12:17
清华大学 C++
out11Man:小丑罢了,不用理会
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务