对数

应用场景

最近有一个需求,要展示某人整个人生阶段中,每一年的支出数据。假设这个人的支出信息如下(为了简化理解和作图,只罗列5年数据):

年龄 支出金额 原因
21 20000 读书
22 30000 读书+毕业
23 50000 工作后生活(吃饭、租房等)
24 3050000 (家里出钱)买房
25 60000 工作后生活(吃饭、租房等,物价上涨了一些)

如果按照普通的数值作图,得到的图形是这样的:

not_log

可以看到,因为24岁这一年买房的支出远大于其他年份的支出,导致其他年份的数据都被压到底部了,无法区分出来具体数值。

而实际上买房这种属于非常规消费,在人生阶段中出现的频率是很低的,不应该由于这少部分年份的数据,而影响了大部分年份的展示。

为了解决这类非常规数据,就需要引入对数了。

这里先展示下log做粗糙处理后的图形(没有考虑0-1的情况,后面会详细说明下这个区间的特殊处理):

with_log

可以看到,非常规数据的差异被缩小了,大部分数据的趋势都可以明显的看出来。

对数处理,用于解决大部分常规数据+小部分非常规数据(即数据不是同一个量级)混合展示的问题。

什么是对数

log

对数的定义,摘自百度百科:

如果 ,N=ax(a>0, a≠1),即a的x次方等于N(a>0,且a≠1),那么数x叫做以a为底N的对数(logarithm),记作x=logaN。其中,a叫做对数的底数,N叫做真数,x叫做“以a为底N的对数”。

  1. 特别地,我们称以10为底的对数叫做常用对数(common logarithm),并记为lg。
  2. 称以无理数e(e=2.71828…)为底的对数称为自然对数(natural logarithm),并记为ln。
  3. 零没有对数。
  4. 实数范围内,负数无对数。在虚数范围内,负数是有对数的。

实现思路

JavaScript中已经提供了现成静态函数了:Math.log()

比如求以a为底的N的对数,则可以这样计算:

1
let x = Math.log(N) / Math.log(a)

我们实际作图的时候,其实是先算出每个数值对应的以10为底的对数,然后用这些对数值作图的。

以上面这个图形为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 原始数据
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,那么:

1
let g = Math.log(N + 1) / Math.log(a)

如何解决真数为负数的问题

由于只能对整数取对数,当真数存在负值时,就要做一些特殊处理了。

其实这个处理很简单,就是取其绝对值来计算对数,然后将算得的结果乘以-1,就是作图所需的图值了:

1
let g = (Math.log(Math.abs(N) + 1) / Math.log(a)) * -1

流程图

这里借用下同事ZHQ的流程图,画得很棒,清晰明了:

process

其他问题

如何解决刻度显示问题

在展示的时候通过提供的格式化回调函数,对数值进行幂计算(Math.pow()),还原为原始数值进行展示即可。

比如Y刻度的数值,可以这样处理:

1
2
3
4
5
6
7
8
9
10
11
12
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计算出来的不是整数,因此别忘记了做精度处理。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
<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>