对数
应用场景
最近有一个需求,要展示某人整个人生阶段中,每一年的支出数据。假设这个人的支出信息如下(为了简化理解和作图,只罗列5年数据):
| 年龄 | 支出金额 | 原因 |
|---|---|---|
| 21 | 20000 | 读书 |
| 22 | 30000 | 读书+毕业 |
| 23 | 50000 | 工作后生活(吃饭、租房等) |
| 24 | 3050000 | (家里出钱)买房 |
| 25 | 60000 | 工作后生活(吃饭、租房等,物价上涨了一些) |
如果按照普通的数值作图,得到的图形是这样的:

可以看到,因为24岁这一年买房的支出远大于其他年份的支出,导致其他年份的数据都被压到底部了,无法区分出来具体数值。
而实际上买房这种属于非常规消费,在人生阶段中出现的频率是很低的,不应该由于这少部分年份的数据,而影响了大部分年份的展示。
为了解决这类非常规数据,就需要引入对数了。
这里先展示下log做粗糙处理后的图形(没有考虑0-1的情况,后面会详细说明下这个区间的特殊处理):

可以看到,非常规数据的差异被缩小了,大部分数据的趋势都可以明显的看出来。
对数处理,用于解决大部分常规数据+小部分非常规数据(即数据不是同一个量级)混合展示的问题。
什么是对数

对数的定义,摘自百度百科:
如果 ,N=ax(a>0, a≠1),即a的x次方等于N(a>0,且a≠1),那么数x叫做以a为底N的对数(logarithm),记作x=logaN。其中,a叫做对数的底数,N叫做真数,x叫做“以a为底N的对数”。
实现思路
JavaScript中已经提供了现成静态函数了:Math.log()
比如求以a为底的N的对数,则可以这样计算:
let x = Math.log(N) / Math.log(a)
我们实际作图的时候,其实是先算出每个数值对应的以10为底的对数,然后用这些对数值作图的。
以上面这个图形为例:
// 原始数据
var raw = [
['21岁', 20000],
['22岁', 30000],
['23岁', 50000],
['24岁', 3050000],
['25岁', 60000]
];
// 实际画图用的数据
raw.forEach((d) => {
d[1] = Math.log(d[1]) / Math.log(10)
})
/*
["21岁", 4.30102999566398],
["22岁", 4.477121254719662],
["23岁", 4.698970004336019],
["24岁", 6.484299839346785],
["25岁", 4.778151250383643]
/*
处理流程
如何解决0-1之间的真数,其对数为负数的问题
以10为底的对数
| 对数 | 真数 |
|---|---|
| -∞ | 0 |
| 0 | 1 |
| 1 | 10 |
| 2 | 100 |
| 3 | 1000 |
真数为0时,对数是负无穷,而我们实际作图的时候,需要将真数0对应到对数0上面,这样才能做出标准的0点。因此我们数据处理的时候,还需要先将真数做一个加一操作,使真数从1开始,也就是取得的对数从0开始。
假如我们将真正绘制图形的Y值叫做图值g,那么:
let g = Math.log(N + 1) / Math.log(a)
如何解决真数为负数的问题
由于只能对整数取对数,当真数存在负值时,就要做一些特殊处理了。
其实这个处理很简单,就是取其绝对值来计算对数,然后将算得的结果乘以-1,就是作图所需的图值了:
let g = (Math.log(Math.abs(N) + 1) / Math.log(a)) * -1
流程图
这里借用下同事ZHQ的流程图,画得很棒,清晰明了:

其他问题
如何解决刻度显示问题
在展示的时候通过提供的格式化回调函数,对数值进行幂计算(Math.pow()),还原为原始数值进行展示即可。
比如Y刻度的数值,可以这样处理:
label: {
show: true,
style: {
fill: 'rgba(0, 0, 0, 1)',
fontFamily: 'SFUIText-Regular'
},
formatter: (d) => {
// 别忘了精度处理
let value = Math.ceil(Math.pow(10, d))
return value >= 10000 ? (value / 10000) + '万' : value
}
},
由于JS计算出来的不是整数,因此别忘记了做精度处理。
完整代码
<head>
<link rel="stylesheet" href="http://s.thsi.cn/js/d3chart/style/v0.0.9/index.css">
<script src="http://s.thsi.cn/js/datav/charts/latest/d3_charts.js"></script>
</head>
<body>
<div style="width:1000px;height:350px;background:#fff" id="chart"></div>
<script>
/**
* 常规数值
*/
var data2 = [
['21岁', 20000],
['22岁', 30000],
['23岁', 50000],
['24岁', 3050000],
['25岁', 60000]
];
/**
* 对数数值
*/
data2.forEach((d) => {
// 注意这里的+1操作,是为了解决0-1之间对数为负数的问题
d[1] = Math.log(d[1] + 1) / Math.log(10)
})
var hasNegativeNumber = false;
for (var i = data2.length; i--;) {
if (data2[i][1] < 0 || data2[i][2] < 0) {
hasNegativeNumber = true;
break;
}
}
var domainFormatter1 = function (domain) {
if (hasNegativeNumber) {
var max = Math.max(Math.abs(domain[0]), Math.abs(domain[1]));
return [-max, max];
} else {
return [domain[0] * 0.8, domain[1] * 1.2];
}
};
var domainFormatter2 = function (domain) {
if (hasNegativeNumber) {
var max = Math.max(Math.abs(domain[0]), Math.abs(domain[1]));
return [-max, max];
} else {
return [domain[0] * 0.8, domain[1] * 1.4];
}
};
option = {
axis: [{
position: 'bottom',
$dataIndex: 0,
dataKey: 0,
line: {
show: false
},
tick: {
show: false,
style: {
fill: 'rgba(94,120,145,1)'
}
},
label: {
style: {
fill: 'rgba(0, 0, 0, 1)',
fontFamily: 'SFUIText-Regular',
rich: {
a: {
fontSize: 14,
textLineHeight: 18
}
}
},
formatter: function (text) {
return '{a|' + text + '}';
},
padding: 20,
// rotate: 45
},
splitLine: {
show: true
},
interval: 1
},
{
position: 'left',
type: 'linear',
xOrY: 'y',
line: {
show: false
},
tick: {
show: true,
style: {
fill: 'rgba(0, 0, 0, 0.3)',
}
},
label: {
show: true,
style: {
fill: 'rgba(0, 0, 0, 1)',
fontFamily: 'SFUIText-Regular'
},
formatter: (d) => {
let value = Math.pow(10, d)
return value >= 10000 ? (value / 10000) + '万' : value
},
},
splitLine: {
show: true,
"style": {
"color": "#e4e4e4",
"lineWidth": 1
}
},
domainFormatter: domainFormatter1,
nice: true
}
],
grid: [{
bottom: '15%',
right: '12%',
left: '10%',
top: '10%'
}],
series: [{
type: 'bar',
$dataIndex: 0,
dataKey: 1,
label: {
normal: {
show: true,
formatter: (d) => {
// 精度处理
let value = Math.ceil(Math.pow(10, d))
// 注意这里的-1,是为了去掉之前处理[0, 1]区间数据时加上的那个1
return value >= 10000 ? ((value - 1) / 10000) + '万' : (value - 1)
}
},
emphasis: {
show: true,
style: {
position: 'top',
fill: 'rgba(70,218,218,1)',
fontFamily: 'PingFang-SC-Semibold'
}
}
},
itemStyle: {
normal: {
fill: {
y2: 1,
x2: 0,
colorStops: [{
offset: 0,
color: 'rgba(70,228,228,1)'
},
{
offset: 1,
color: 'rgba(70,218,218,1)'
}
]
}
}
}
}],
data: [{
originData: data2
}]
};
var myChart = D3Charts.init("chart");
myChart.setOption(option);
</script>
</body>