Skip to content

Commit

Permalink
feat: 连续数值刻度算法
Browse files Browse the repository at this point in the history
  • Loading branch information
cycgit committed Aug 11, 2020
1 parent 9be8ccb commit 83b1d34
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 0 deletions.
94 changes: 94 additions & 0 deletions src/chart/controller/linear-tick.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@

import { fixedBase } from '@antv/util';
import { isNil } from '../../util/common';
import AutoUtil from './scale-util';
const { snapFactorTo, snapMultiple } = AutoUtil;

// 默认经验刻度
const SNAP_COUNT_ARRAY = [ 1, 1.2, 1.5, 1.6, 2, 2.2, 2.4, 2.5, 3, 4, 5, 6, 7.5, 8, 10 ];
const DEFAULT_TICK_COUNT = 5;
const EPS = 1e-12;

// linear连续数值轴刻度nice算法
export default info => {

let { min, max, interval, tickCount = DEFAULT_TICK_COUNT } = info;
// 用户传入的经验刻度
const snapArray = info.snapArray || SNAP_COUNT_ARRAY;

if (Math.abs(max - min) < EPS) {
if (min === 0) {
max = 1;
} else {
if (min > 0) {
min = 0;
} else {
max = 0;
}
}
if (max - min < 5 && !interval && max - min >= 1) {
interval = 1;
}
}

if (isNil(interval)) {
// 计算间距
const temp = (max - min) / (tickCount - 1);
interval = snapFactorTo(temp, snapArray, 'ceil');
}

// 如果有自定义间隔
if (info.interval) {
// 校正 max 和 min
max = snapMultiple(max, interval, 'ceil'); // 向上逼近
min = snapMultiple(min, interval, 'floor'); // 向下逼近

tickCount = Math.round((max - min) / interval);
min = fixedBase(min, interval); // 当min为负数的时候,fixedBase后,min可能会大于minLimit,导致最终产出的tick是大于minLimit的,所以必须进行修正
max = fixedBase(max, interval);
} else {
const avg = (max + min) / 2;
const avgTick = snapMultiple(avg, interval, 'ceil');
const sideCount = Math.floor((tickCount - 2) / 2);
let maxTick = fixedBase(avgTick + sideCount * interval, interval);
let minTick;
if (tickCount % 2 === 0) {
minTick = fixedBase(avgTick - sideCount * interval, interval);
} else {
minTick = fixedBase(avgTick - (sideCount + 1) * interval, interval);
}

let prevMaxTick = null;
// 顶部tick矫正
// 如果减去intervl, fixBase后,新的minTick没有大于之前的值,就退出,防止死循环
while (maxTick < max && (prevMaxTick === null || maxTick > prevMaxTick)) { // 保证计算出来的刻度最大值 maxTick 不小于数据最大值 max
prevMaxTick = maxTick;
maxTick = fixedBase(maxTick + interval, interval);
}

let prevMinTick = null;
// 底部tick矫正
// 如果减去intervl, fixBase后,新的minTick没有小于之前的值,就退出,防止死循环
while (minTick > min && (prevMinTick === null || minTick < prevMinTick)) { // 保证计算出来的刻度最小值 minTick 不大于数据最小值 min
prevMinTick = minTick;
minTick = fixedBase(minTick - interval, interval); // 防止超常浮点数计算问题
}
max = maxTick;
min = minTick;
}

const ticks = [];

ticks.push(min);
for (let i = 1; i < tickCount; i++) {
const tickValue = fixedBase(interval * i + min, interval);
if (tickValue < max) {
ticks.push(tickValue);
}
}
if (ticks[ticks.length - 1] < max) {
ticks.push(max);
}

return ticks;
};
174 changes: 174 additions & 0 deletions src/chart/controller/scale-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
const DECIMAL_LENGTH = 12;
// 获取系数
function getFactor(v) {
let factor = 1;
if (v === Infinity || v === -Infinity) {
throw new Error('Not support Infinity!');
}
if (v < 1) {
let count = 0;
while (v < 1) {
factor = factor / 10;
v = v * 10;
count++;
}
// 浮点数计算出现问题
if (factor.toString().length > DECIMAL_LENGTH) {
factor = parseFloat(factor.toFixed(count));
}
} else {
while (v > 10) {
factor = factor * 10;
v = v / 10;
}
}

return factor;
}

// 取小于当前值的
function arrayFloor(values, value) {
const length = values.length;
if (length === 0) {
return NaN;
}

let pre = values[0];

if (value < values[0]) {
return NaN;
}

if (value >= values[length - 1]) {
return values[length - 1];
}
for (let i = 1; i < values.length; i++) {
if (value < values[i]) {
break;
}
pre = values[i];
}

return pre;
}

// 大于当前值的第一个
function arrayCeiling(values, value) {
const length = values.length;
if (length === 0) {
return NaN;
}
// var pre = values[0];
let rst;
if (value > values[length - 1]) {
return NaN;
}
if (value < values[0]) {
return values[0];
}

for (let i = 1; i < values.length; i++) {
if (value <= values[i]) {
rst = values[i];
break;
}
}

return rst;
}


const Util = {
// 获取逼近的数值
snapFactorTo(v, arr, snapType) { // 假设 v = -512,isFloor = true
if (isNaN(v)) {
return NaN;
}
let factor = 1; // 计算系数
if (v !== 0) {
if (v < 0) {
factor = -1;
}
v = v * factor; // v = 512
const tmpFactor = getFactor(v);
factor = factor * tmpFactor; // factor = -100

v = v / tmpFactor; // v = 5.12
}
if (snapType === 'floor') {
v = Util.snapFloor(arr, v); // v = 5
} else if (snapType === 'ceil') {
v = Util.snapCeiling(arr, v); // v = 6
} else {
v = Util.snapTo(arr, v); // 四舍五入 5
}

let rst = parseFloat((v * factor).toPrecision(DECIMAL_LENGTH)); // 如果出现浮点数计算问题,需要处理一下

// 如果出现浮点数计算问题,需要处理一下
if (Math.abs(factor) < 1 && rst.toString().length > DECIMAL_LENGTH) {
const decimalVal = parseInt(1 / factor);
const symbol = factor > 0 ? 1 : -1;
rst = v / decimalVal * symbol;
}
return rst;
},
// 获取逼近的倍数
snapMultiple(v, base, snapType) {
let div;
if (snapType === 'ceil') {
div = Math.ceil(v / base);
} else if (snapType === 'floor') {
div = Math.floor(v / base);
} else {
div = Math.round(v / base);
}
return div * base;
},
/**
* 获取逼近的值,用于对齐数据
* @param {Array} values 数据集合
* @param {Number} value 数值
* @return {Number} 逼近的值
*/
snapTo(values, value) {
// 这里假定values是升序排列
const floorVal = arrayFloor(values, value);
const ceilingVal = arrayCeiling(values, value);
if (isNaN(floorVal) || isNaN(ceilingVal)) {
if (values[0] >= value) {
return values[0];
}
const last = values[values.length - 1];
if (last <= value) {
return last;
}
}
if (Math.abs(value - floorVal) < Math.abs(ceilingVal - value)) {
return floorVal;
}
return ceilingVal;
},
/**
* 获取逼近的最小值,用于对齐数据
* @param {Array} values 数据集合
* @param {Number} value 数值
* @return {Number} 逼近的最小值
*/
snapFloor(values, value) {
// 这里假定values是升序排列
return arrayFloor(values, value);
},
/**
* 获取逼近的最大值,用于对齐数据
* @param {Array} values 数据集合
* @param {Number} value 数值
* @return {Number} 逼近的最大值
*/
snapCeiling(values, value) {
// 这里假定values是升序排列
return arrayCeiling(values, value);
}
};

export default Util;
3 changes: 3 additions & 0 deletions src/chart/controller/scale.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { getScale, registerTickMethod } from '@antv/scale';
import { isNil, mix, isObject, each, isArray, isString, isNumber, Array } from '../../util/common';
import Global from '../../global';
import TimeCat from './timecat-tick';
import LinearTick from './linear-tick';
// 覆盖0.3.x的 timecat scale算法
registerTickMethod('time-cat', TimeCat);
// 覆盖linear 度量的tick算法
registerTickMethod('wilkinson-extended', LinearTick);

function isFullCircle(coord) {
if (!coord.isPolar) {
Expand Down
49 changes: 49 additions & 0 deletions test/unit/chart/controller/line-tick-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as F2 from '../../../../src/index-all';

const canvas = document.createElement('canvas');
canvas.width = 500;
canvas.height = 500;
canvas.id = 'ctx';
document.body.appendChild(canvas);

describe('刻度轴算法', function() {

it('刻度轴', function() {

const data = [
{ genre: 'Sports', sold: 275 },
{ genre: 'Strategy', sold: 115 },
{ genre: 'Action', sold: 120 },
{ genre: 'Shooter', sold: 350 },
{ genre: 'Other', sold: 150 }
];

const chart = new F2.Chart({
id: 'ctx',
pixelRatio: window.devicePixelRatio // 指定分辨率
});

chart.source(data);

chart.scale('date', {
type: 'timeCat',
range: [ 0, 1 ],
tickCount: 3,
mask: 'MM-DD'
// formatter: value => value.substr(5) // 去掉 yyyy-mm-dd 的年份
});


// chart.scale('sold', {
// tickCount: 5,
// });

chart.interval()
.position('genre*sold')
.color('genre');

chart.render();

});

});

0 comments on commit 83b1d34

Please sign in to comment.