GithubHelp home page GithubHelp logo

antvis / scale Goto Github PK

View Code? Open in Web Editor NEW
56.0 44.0 17.0 657 KB

📦 Toolkit for mapping abstract data into visual representation.

Home Page: https://observablehq.com/@antv/aloha-antv-scale

License: MIT License

JavaScript 0.78% TypeScript 99.15% Shell 0.07%
scale mapping ticks visualization

scale's People

Contributors

candy-tong avatar daydayhappychao avatar hustcc avatar lvisei avatar pearmini avatar pepper-nice avatar visiky avatar yuzhanglong avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

scale's Issues

增加 getFormatter 方法

@hustcc @yuzhanglong 这里我觉得可能 continuous 和 time 比例尺 需要增加一个方法:getFormatter,该方法返回用于展示 ticks 时候的格式化函数。因为在确定 tick 的展示形式的时候,可以能需要比例尺根据生成的 ticks 的数量和分布去推荐。

  • time :当 domain 的值很近的时候只展示分钟,很远的时候展示年份。
  • continuous :ticks 的精度其实也有相应的问题,这些 formatter 其实根据生成的 ticks 来确定的。
import { Linear } from '@antv/scale';

// 不设置 formatter,返回默认推荐的 formatter
const x = new Linear();
const defaultFormatter = x.getFormatter();
defaultFormatter(x.map(0.33333)); // '0.3'

// 设置 formatter, 就返回该 formatter
x.update({
  formatter: x => x + '';
});

const customFormatter = x.getFormatter();
customFormatter(x.map(0.33333)); // '0.33333'

scale 的calculateTicks方法需要优化

  • Link: CodePen
  • Platform:
  • Mini Showcase(like screenshots):

1. 出现了科学计数法的数值
image

2. 计算出来的ticks无法包含用户的全部数据
image

3. 设置tickCount后,计算出来的ticks长度不等于tickCount
image

log nice

当前 log 的 nice 算法其实也有问题,因为它可能把 domain nice 成 [0, x],对于 log 来说如果 domain 为 [0, x],map 会返回 NaN。需要参考 d3 log 的 nice 算法。

🐛[BUG] @ant-design/plots 1.2.6版本 DualAxes中折线图y轴标签出现极端小数刻度,使用yAxis.label.formatter无法更改

Snipaste_2024-03-08_11-48-47

代码如下
`import { DualAxes } from '@ant-design/plots';
import React from 'react';

const CommonDualAxes: React.FC = () => {
const data = [
[
{
xval: '20240226',
yval: 5.21,
day: '2024-02-26',
count: 5.21,
category: 'FEV1',
},
],
[
{
xval: '20240226',
yval: 87,
day: '2024-02-26',
count: 87,
category: 'FEV1/预计值',
},
{
xval: '20240226',
yval: 87.11,
day: '2024-02-26',
count: 87.11,
category: 'FEV1/FVC',
},
],
];
const columnChartConfig = {
geometry: 'column',
xField: 'day',
yField: 'count',
seriesField: 'category',
autoFit: true,
isGroup: true,
lineStyle: { lineWidth: 2, lineDash: [4, 5] },
legend: {
position: 'bottom',
marker: (_, index) => ({
symbol: 'circle',
style: {
r: 4,
},
}),
},
maxColumnWidth: 5,
minColumnWidth: 5,
columnStyle: {
radius: [20, 20, 0, 0],
},
xAxis: {
line: {
style: {
stroke: '#E4ECF7',
},
},
},
yAxis: {
tickCount: 5,
grid: {
line: {
style: {
lineDash: [4, 4],
lineWidth: 1,
stroke: '#E4ECF7',
},
},
},
label: {
style: {
fill: '#98a9bc',
},
},
},
};

const lineChartConfig = {
geometry: 'line',
xField: 'day',
yField: 'count',
seriesField: 'category',
smooth: true,
autoFit: true,
legend: {
position: 'bottom',
marker: (_: any, index: number, arr: any) => {
return {
symbol: 'circle',
style: {
r: 3,
fill: arr?.style?.stroke,
},
};
},
},
xAxis: {
type: 'timeCat',
line: {
style: {
stroke: '#E4ECF7',
},
},
},
yAxis: {
tickCount: 5,
grid: {
line: {
style: {
lineDash: [4, 4],
lineWidth: 1,
stroke: '#E4ECF7',
},
},
},
label: {
style: {
fill: '#98a9bc',
},
formatter: (n) => {
return Number(n).toFixed(2);
},
},
},
};
const config: any = {
data,
autoFit: true,
isArea: true,
xField: 'day',
yField: ['count', 'count'],
seriesField: 'category',
legend: {
position: 'bottom',
},
yAxis: [
{
tickCount: 5,
grid: {
line: {
style: {
lineDash: [4, 4],
lineWidth: 1,
stroke: '#E4ECF7',
},
},
},
label: {
style: {
fill: '#98a9bc',
},
formatter: (n) => {
return Number(n).toFixed(2);
},
},
},
{
tickCount: 5,
label: {
style: {
fill: '#98a9bc',
},
},
},
],
xAxis: {
line: {
style: {
stroke: '#E4ECF7',
},
},
label: {
style: {
fill: '#98a9bc',
},
},
},
geometryOptions: [columnChartConfig, lineChartConfig],
};
return <DualAxes {...config} />;
};

export default CommonDualAxes;
`

Add Sequential Scale

Sequential Scale

Sequential Scales are like linear scales, except that they use interpolator function rather than an array to describe the visual domain. And they also do not expose the invert method and interpolate options.

For every value to be scaled, there are two steps in short:

  • Normalize: Map x from [min, max] to [0, 1].
  • Interpolate: Apply interpolator to the normalized value.

Basic Usage

import { Sequential } from '@antv/scale';

function interpolator(t) {
  return 1 - t;
}

const scale = new Sequential({
  domain: [0, 10],
  interpolator: interpolator,
});

scale.map(5); // 0.5
scale.map(2); // 0.8
scale.map(8); // 0.2
scale.getOptions().range; // [1, 0]

API Design

export type SequentialOptions = Omit<LinearOptions, 'Interpolates'> & { interpolator: Interpolate };
  • scale.map(x)
  • scale.getTicks()
  • scale.update(options)
  • scale.getOptions()
  • scale.nice()
  • scale.clone()

Implement Suggestions

Note: This is just some suggestions, feel free to implement them in your own way if you have a better idea.

Use decoration instead of inheritance to enhance linear scale. Although this will cause inconsistent design patterns, it is good for future scales, such as SequentialLog, SequentialPow, SequentialSqrt, etc,.

// src/sequential.ts
import { Linear, Log } from './linear',

function Sequentialish(Scale) {
  // Modify the behavior of public method map directly.
  Scale.prototype.map = function(x) {}

  // Or modify the behavior of protected method rescale to get new output function.
  Scale.prototype.rescale = function() {
    // ...
    this.ouput = /* ... */;
  }

  Scale.prototype.invert = undefined;
}

// Define the shape of Sequential object explicitly.
interface Sequential {}
 
export const Sequential = Sequentialish(Linear);

Example In G2

image

G2.render({
  type: 'area',
  data: [],
  transform: [{ type: 'stackY', }],
  scale: {
    // Which one is better?
    // color: { type: 'linear', range: d3.interpolateCool }
    // color: { type: 'sequential', interpolator: d3.interpolateCool },
   color: {/* ... */},
  },
  encode: {
    x: 'year',
    y: 'value',
    color: () => Math.random()
  },
})

Reference

For more details, read the source code or doc of d3-scale.

Todo

  • source
  • test: coverage 100%
  • docs

G2Plot迁移Scale 3.0相关问题

  • Link:
  • Platform:
  • Mini Showcase(like screenshots):
  • timeScale默认 'YYYY-MM-DD' 的mask
 { year: "1850", 
   value: 0, 
   category: "Liquid fuel"
 }

屏幕快照 2020-02-04 下午3 46 34

  • linearScale的min和max设置失效

屏幕快照 2020-02-04 下午4 22 53

继承相关的问题

这里有个继承相关的问题我不知道如何解决,大家来看看!@hustcc @yuzhanglong

// 这样写是为了共有 API 的类型推断,不传入泛型就需要在每一个子类中为共有共有方法添加类型
class Base<O extends BaseOptions> {/* ... */};

// 这样写是应为还有子类继承自 Ordinal
class Ordinal<O extends OrdinalOptions = OrdinalOptions> extends Base<O> {/ * ... */ };
// 下面的写法会报错
const x = new Ordinal({
  range: [0, 1]
});

// 像下面一样调用 update 的时候报错
// 报错内容: Argument of type '{ range: number[]; }' is not assignable to parameter of type 'Partial<{ domain: number[] }>'.  
// 看上去是因为在实例化 Ordinal 没有传入泛型(默认泛型好像没有用),导致根据传入的 Options 推导了 O 的类型
// 所以 update 的时候不兼容
x.update({
  domain: [0, 1]
})
// 下面的写法没有问题
const x = new Ordinal<OrdinalOptions>({
  range: [0, 1]
});

x.update({
  domain: [0, 1]
})
// 这样写也没有问题
const options: OrdinalOptions = {
  range: [0, 1]
}

const x = new Ordinal(options);

x.update({
  domain: [0, 1]
})

time cat ticks计算不准

  • Link:
  • Platform:
  • Mini Showcase(like screenshots):
const scale = new Scale({
  values: [1590076800000,1590336000000,1590422400000,1590508800000,1590595200000,1590681600000,1590940800000,1591027200000,1591113600000,1591200000000,1591286400000,1591545600000,1591632000000,1591718400000,1591804800000,1591891200000,1592150400000,1592236800000,1592323200000,1592409600000,1592496000000,1592755200000,1592841600000,1592928000000],
  tickCount: 3,
});

实际算出来ticks为4个

添加 useRelation 工具函数

添加 useRelation 工具函数

useRelation 主要用于增加所有比例尺的条件映射能力。条件映射能力就是在输入满足指定条件的时候返回期望的值,在不满足条件的情况下走比例尺的默认逻辑。比如在下面的热力图的例子中,需要当值为 NaN 的时候为灰色,在值为 0 的时候为白色,其他的时候就走比例尺的默认映射逻辑。也可以看这个 antvis/G2Plot#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];
}

Add Diverging Scale

Diverging Scale

Diverging scale is like sequential scale, which expects it has three elements to describe visual domain by interpolator. Their domain includes three values: two extremes and a central point. They also do not expose the invert method and interpolate options.

For every value to be scaled, there are two steps in short:

  • Normalize: Map x from [min, center, max] to [0, 0.5, 1].
  • Interpolate: Apply interpolator to the normalized value.

Basic Usage

import { Diverging } from '@antv/scale';

const scale = new Sequential({
  domain: [-10, 0, 10],
  interpolator: t => 1-t
});

scale.map(5); // 0.25
scale.map(-5); // 0.75
scale.map(2); // 0.4
scale.getOptions().range; // [1, 0.5, 0]

API Design

  • scale.map(x)
  • scale.getTicks()
  • scale.update(options)
  • scale.getOptions()
  • scale.nice()
  • scale.clone()

Implement

Like Sequential Scale, use decoration instead of inheritance to enhance linear scale.

import { Linear } from './Linear';

function Diverginglish(Scale){
  return class Diverging extends Scale {
    map(){}
    rescale(){}
    invert = undefined;
  }
};

export const Diverging = Diverginglish(Scale);

Reference

v0.4 需要改进的地方

暂时不需要处理,先再看看。

  • map 的参数检查的提取。
  • update 之后的 rescale 提取。
  • test 的规范和一些不必要的 test 的删除。
  • scale 通用的过程的总结并且体现在代码上: options -> state -> map(constructor +update)
  • invert 对于非法参数的统一处理方式。
  • 注释的规范,哪些是要注释,哪些不需要注释,如何注释。
  • tickMethod options 的 type。
  • 调整 types 的结构。
  • 根据代码和注释直接生成文档。
  • nice 是否需要修改原来的 domain:统一修改。

time类型生成的ticks的疑问

假如我有这些数据

["2018-07-30T16:30:00.000Z", "2018-07-31T16:30:00.000Z", "2018-08-01T16:30:00.000Z", "2018-08-02T16:30:00.000Z", "2018-08-03T16:30:00.000Z", "2018-08-04T16:30:00.000Z", "2018-08-05T16:30:00.000Z"]

会生成ticks

["2018-07-31", "2018-08-01", "2018-08-02", "2018-08-03", "2018-08-04", "2018-08-05", "2018-08-06"]

if (tick >= self.min && tick <= self.max) {

由于默认的nice = false,会被上面这行代码过滤掉第一项,会导致显示的ticks少了第一项
image

这是设计上就如此吗?还是代码上的BUG?

[0.3] log scale ticks 问题

复现 demo:

http://localhost:8000/en/examples/point/scatter#time-bubble

import { DataView } from '@antv/data-set';
import { Chart } from '@antv/g2';

fetch('../data/time-scatter.json')
  .then(res => res.json())
  .then(data => {
    const chart = new Chart({
      container: 'container',
      autoFit: true,
      height: 500,
    });

    const dv = new DataView();
    dv.source(data)
      .transform({
        type: 'map',
        callback: obj => {
          obj.exp_amo = obj.exp_amo * 1;
          return obj;
        }
      });

    chart.data(dv.rows);
    chart.animate(false);
    chart.scale({
      exp_dat: {
        type: 'time',
        mask: 'M/YY',
        tickCount: 14
      },
      exp_amo: {
        type: 'log',
        ticks: [225, 1000000, 2000000, 4000000, 6000000]
      }
    });
    chart.legend(false);
    chart.tooltip({
      showTitle: false,
    });
    chart.axis('exp_dat', {
      tickLine: null,
      label: {
        style: {
          fontSize: 14
        }
      }
    });
    chart.axis('exp_amo', {
      tickLine: null,
      line: null,
      grid: {
        line: {
          style: {
            lineDash: null,
            stroke: '#999'
          }
        }
      },
      label: {
        formatter: val => {
          let formatted;
          if (+val === 225) {
            formatted = 0;
          } else {
            formatted = +val / 1000000;
          }
          return '$' + formatted + 'M';
        }
      }
    });
    chart.point()
      .position('exp_dat*exp_amo')
      .size('exp_amo', [1, 10])
      .shape('circle')
      .tooltip('exp_dat*can_nam*spe_nam*exp_amo');
    chart.render();
  });

Band

  • Split computeBandState into computeBandState and computeFlexBandState for performance.
  • Use d3.internMap instead of Map to compute flex band state to handle temporal domain.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.