antvis / scale Goto Github PK
View Code? Open in Web Editor NEW📦 Toolkit for mapping abstract data into visual representation.
Home Page: https://observablehq.com/@antv/aloha-antv-scale
License: MIT License
📦 Toolkit for mapping abstract data into visual representation.
Home Page: https://observablehq.com/@antv/aloha-antv-scale
License: MIT License
@hustcc @yuzhanglong 这里我觉得可能 continuous 和 time 比例尺 需要增加一个方法:getFormatter,该方法返回用于展示 ticks 时候的格式化函数。因为在确定 tick 的展示形式的时候,可以能需要比例尺根据生成的 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'
当前 log 的 nice 算法其实也有问题,因为它可能把 domain nice 成 [0, x],对于 log 来说如果 domain 为 [0, x],map 会返回 NaN。需要参考 d3 log 的 nice 算法。
都修改成 5
代码如下
`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;
`
const x = new Ordinal({
domain: [new Date('2020-02-01'), new Date('2020-02-02'), new Date('2020-02-03')],
range: ['a', 'b', 'c']
});
x.map(new Date('2020-02-02')) // expected: 'b', received: 'a'
related: d3/d3-scale#237
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:
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]
export type SequentialOptions = Omit<LinearOptions, 'Interpolates'> & { interpolator: Interpolate };
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);
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()
},
})
For more details, read the source code or doc of d3-scale.
如标题:pretty.js里的number.parsefloat不兼容IE
IE报错:对象不支持“parseFloat”属性或方法
这里有个继承相关的问题我不知道如何解决,大家来看看!@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]
})
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个
map 和 invert 的不同没有体现出来。
useRelation 主要用于增加所有比例尺的条件映射能力。条件映射能力就是在输入满足指定条件的时候返回期望的值,在不满足条件的情况下走比例尺的默认逻辑。比如在下面的热力图的例子中,需要当值为 NaN 的时候为灰色,在值为 0 的时候为白色,其他的时候就走比例尺的默认映射逻辑。也可以看这个 antvis/G2Plot#3329 里面提到的问题。
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)
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);
}
}
因为这个能力挺常用的,不一定只会在 G2 内部使用,任何使用 @antvis/scale 的库都可能有相同的需求。
Map.get(x)
API 来获得映射之后的值,就失去了 Map 的意义。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];
}
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:
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]
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);
.
暂时不需要处理,先再看看。
原始数据的最大值和最小值分别是:22.57 和 56.23,当设置 tickCount 为 4 时,计算出来的最小值为 24,大于原始数据的最小值
假如我有这些数据
["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"]
Line 132 in ee0efbe
由于默认的nice = false
,会被上面这行代码过滤掉第一项,会导致显示的ticks少了第一项
这是设计上就如此吗?还是代码上的BUG?
参考这个 issue:antvis/G2#4867 ,主要有两个问题:
scale/src/tick-methods/d3-log.ts
Line 40 in 7a49d81
// This result in unexpected mapping.
const scale = new Point({
paddingInner: 0.2
});
复现 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();
});
ticks [0, 1] 的结果无法等分,期待结果 [0, 0.25, 0.5, 0.75, 1]
repro:https://codesandbox.io/s/compassionate-bartik-kpdw83?file=/src/App.js
究其原因是 prettyNumber
处理的太简单了,是否考虑引入一些大数库来解决这个问题?
wilkinsonExtended(0, 1.2, 5);
// [0, 0.30000000000000004, 0.6000000000000001, 0.9000000000000001, 1.2000000000000002]
computeBandState
into computeBandState
and computeFlexBandState
for performance.tickCount 设置为 6 时,在 https://github.com/antvis/scale/blob/master/src/auto/util.js#L110 处出现了浮点数计算问题导致坐标刻度计算问题。
https://github.com/antvis/scale/blob/master/src/util/time.ts#L16-L31
理由:
toTimeStamp
可以处理,则不需要预先处理数据A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.