智能计算图表刻度位置
无论我使用 matplotlib、Open-Flash-Charts 还是其他图表框架,我总是需要找到一种方法来设置 x/y 轴的范围和间隔,因为内置的功能总是不够智能(或者根本不智能...)
你可以在 pylab(ipython -pylab)中试试这个,看看我想表达的意思:
In [1]: a, b, x = np.zeros(10), np.ones(10), np.arange(10)
In [2]: plot(x, a); plot(x, b)
你会看到一个空的框架网格,只能看到上下边框下的两条水平线。
我在想有没有什么算法可以用(我可以移植到 Python 中),来智能地设置上下 y 轴的限制和步长,同时计算 x 轴上每隔多少个值显示一次刻度。
举个例子,假设我有 475 个测量值,格式是 (datetime, temperature)
,也就是 (x, y)
,每个测量值间隔
2011-01-15 10:45:00 < datetime < 2011-01-17 02:20:00
(每 5 分钟一次),还有
26.5 < temperature < 28.3
我对这种情况的建议是设置:
26.4 <= y_scale <= 28.4
,每.2
为一个刻度
并且在 x_scale
上每 12 个项目显示一个刻度(每小时一次)。
但是如果我只有 20 个测量值,分布在 20 天,温度范围是 -21.5 < temperature < 38.7
,那该怎么办呢?有没有什么标准的方法可以用呢?
5 个回答
下面是我的Python代码,用来自动计算刻度,它需要数据的范围和最大刻度数。
举个例子:
auto_tick([-120, 580], max_tick=10, tf_inside=False)
Out[224]: array([-100., -0., 100., 200., 300., 400., 500.])
auto_tick([-120, 580], max_tick=20, tf_inside=False)
Out[225]: array([-100., -50., -0., 50., 100., 150., 200., 250., 300., 350., 400., 450., 500., 550.])
下面是这个函数的Python代码
def auto_tick(data_range, max_tick=10, tf_inside=False):
"""
tool function that automatically calculate optimal ticks based on range and the max number of ticks
:param data_range: range of data, e.g. [-0.1, 0.5]
:param max_tick: max number of ticks, an interger, default to 10
:param tf_inside: True/False if only allow ticks to be inside
:return: list of ticks
"""
data_span = data_range[1] - data_range[0]
scale = 10.0**np.floor(np.log10(data_span)) # scale of data as the order of 10, e.g. 1, 10, 100, 0.1, 0.01, ...
list_tick_size_nmlz = [5.0, 2.0, 1.0, 0.5, 0.2, 0.1, 0.05, 0.02, 0.01] # possible tick sizes for normalized data in range [1, 10]
tick_size_nmlz = 1.0 # initial tick size for normalized data
for i in range(len(list_tick_size_nmlz)): # every loop reduces tick size thus increases tick number
num_tick = data_span/scale/list_tick_size_nmlz[i] # number of ticks for the current tick size
if num_tick > max_tick: # if too many ticks, break loop
tick_size_nmlz = list_tick_size_nmlz[i-1]
break
tick_size = tick_size_nmlz * scale # tick sizse for the original data
ticks = np.unique(np.arange(data_range[0]/tick_size, data_range[1]/tick_size).round())*tick_size # list of ticks
if tf_inside: # if only allow ticks within the given range
ticks = ticks[ (ticks>=data_range[0]) * (ticks<=data_range[1])]
return ticks
我在这里分享一下我用Python写的版本,可能对某些人有帮助:
import math
def nice_number(value, round_=False):
'''nice_number(value, round_=False) -> float'''
exponent = math.floor(math.log(value, 10))
fraction = value / 10 ** exponent
if round_:
if fraction < 1.5:
nice_fraction = 1.
elif fraction < 3.:
nice_fraction = 2.
elif fraction < 7.:
nice_fraction = 5.
else:
nice_fraction = 10.
else:
if fraction <= 1:
nice_fraction = 1.
elif fraction <= 2:
nice_fraction = 2.
elif fraction <= 5:
nice_fraction = 5.
else:
nice_fraction = 10.
return nice_fraction * 10 ** exponent
def nice_bounds(axis_start, axis_end, num_ticks=10):
'''
nice_bounds(axis_start, axis_end, num_ticks=10) -> tuple
@return: tuple as (nice_axis_start, nice_axis_end, nice_tick_width)
'''
axis_width = axis_end - axis_start
if axis_width == 0:
nice_tick = 0
else:
nice_range = nice_number(axis_width)
nice_tick = nice_number(nice_range / (num_ticks - 1), round_=True)
axis_start = math.floor(axis_start / nice_tick) * nice_tick
axis_end = math.ceil(axis_end / nice_tick) * nice_tick
return axis_start, axis_end, nice_tick
使用方法:
>>> nice_bounds(26.5, 28.3)
(26.4, 28.4, 0.2)
另外,我还添加了一个JavaScript的版本:
function nice_number(value, round_){
//default value for round_ is false
round_ = round_ || false;
// :latex: \log_y z = \frac{\log_x z}{\log_x y}
var exponent = Math.floor(Math.log(value) / Math.log(10));
var fraction = value / Math.pow(10, exponent);
if (round_)
if (fraction < 1.5)
nice_fraction = 1.
else if (fraction < 3.)
nice_fraction = 2.
else if (fraction < 7.)
nice_fraction = 5.
else
nice_fraction = 10.
else
if (fraction <= 1)
nice_fraction = 1.
else if (fraction <= 2)
nice_fraction = 2.
else if (fraction <= 5)
nice_fraction = 5.
else
nice_fraction = 10.
return nice_fraction * Math.pow(10, exponent)
}
function nice_bounds(axis_start, axis_end, num_ticks){
//default value is 10
num_ticks = num_ticks || 10;
var axis_width = axis_end - axis_start;
if (axis_width == 0){
axis_start -= .5
axis_end += .5
axis_width = axis_end - axis_start
}
var nice_range = nice_number(axis_width);
var nice_tick = nice_number(nice_range / (num_ticks -1), true);
var axis_start = Math.floor(axis_start / nice_tick) * nice_tick;
var axis_end = Math.ceil(axis_end / nice_tick) * nice_tick;
return {
"min": axis_start,
"max": axis_end,
"steps": nice_tick
}
}
以下是我多年来一直在用的一个简单且效果不错的方法。虽然是用C语言写的,但转换成Python应该不难。
这个函数是从《Graphic Gems》第一卷中来的。
double NiceNumber (const double Value, const int Round) {
int Exponent;
double Fraction;
double NiceFraction;
Exponent = (int) floor(log10(Value));
Fraction = Value/pow(10, (double)Exponent);
if (Round) {
if (Fraction < 1.5)
NiceFraction = 1.0;
else if (Fraction < 3.0)
NiceFraction = 2.0;
else if (Fraction < 7.0)
NiceFraction = 5.0;
else
NiceFraction = 10.0;
}
else {
if (Fraction <= 1.0)
NiceFraction = 1.0;
else if (Fraction <= 2.0)
NiceFraction = 2.0;
else if (Fraction <= 5.0)
NiceFraction = 5.0;
else
NiceFraction = 10.0;
}
return NiceFraction*pow(10, (double)Exponent);
}
你可以像下面的例子那样使用它,根据你希望显示的主要刻度数量来选择一个“合适”的坐标轴起始和结束值。如果你不在乎刻度的话,可以直接设置为一个固定值(比如:10)。
//Input parameters
double AxisStart = 26.5;
double AxisEnd = 28.3;
double NumTicks = 10;
double AxisWidth;
double NewAxisStart;
double NewAxisEnd;
double NiceRange;
double NiceTick;
/* Check for special cases */
AxisWidth = AxisEnd - AxisStart;
if (AxisWidth == 0.0) return (0.0);
/* Compute the new nice range and ticks */
NiceRange = NiceNumber(AxisEnd - AxisStart, 0);
NiceTick = NiceNumber(NiceRange/(NumTicks - 1), 1);
/* Compute the new nice start and end values */
NewAxisStart = floor(AxisStart/NiceTick)*NiceTick;
NewAxisEnd = ceil(AxisEnd/NiceTick)*NiceTick;
AxisStart = NewAxisStart; //26.4
AxisEnd = NewAxisEnd; //28.4