我正在寻找一种将刻度线放置在轴上的算法,给定显示范围、显示宽度以及测量刻度线字符串宽度的函数。
例如,考虑到我需要在 1e-6 和 5e-6 之间显示以及以像素为单位显示的宽度,算法将确定我应该在 1e-6、2e-6、3e- 处放置刻度线(例如) 6、4e-6和5e-6。给定较小的宽度,它可能会决定最佳放置仅在偶数位置,即 2e-6 和 4e-6(因为放置更多刻度线会导致它们重叠)。
智能算法会优先考虑 10、5 和 2 倍数的刻度线。此外,智能算法会围绕零对称。
由于我不喜欢迄今为止找到的任何解决方案,因此我实现了自己的解决方案。它是 C# 语言,但可以轻松翻译成任何其他语言。
它基本上从可能的步骤列表中选择显示所有值的最小步骤,不会在边缘精确地留下任何值,让您轻松选择要使用的可能步骤(无需编辑丑陋的
if-else if
块) ),并支持任何范围的值。我使用 C# Tuple
返回三个值只是为了快速简单的演示。
private static Tuple<decimal, decimal, decimal> GetScaleDetails(decimal min, decimal max)
{
// Minimal increment to avoid round extreme values to be on the edge of the chart
decimal epsilon = (max - min) / 1e6m;
max += epsilon;
min -= epsilon;
decimal range = max - min;
// Target number of values to be displayed on the Y axis (it may be less)
int stepCount = 20;
// First approximation
decimal roughStep = range / (stepCount - 1);
// Set best step for the range
decimal[] goodNormalizedSteps = { 1, 1.5m, 2, 2.5m, 5, 7.5m, 10 }; // keep the 10 at the end
// Or use these if you prefer: { 1, 2, 5, 10 };
// Normalize rough step to find the normalized one that fits best
decimal stepPower = (decimal)Math.Pow(10, -Math.Floor(Math.Log10((double)Math.Abs(roughStep))));
var normalizedStep = roughStep * stepPower;
var goodNormalizedStep = goodNormalizedSteps.First(n => n >= normalizedStep);
decimal step = goodNormalizedStep / stepPower;
// Determine the scale limits based on the chosen step.
decimal scaleMax = Math.Ceiling(max / step) * step;
decimal scaleMin = Math.Floor(min / step) * step;
return new Tuple<decimal, decimal, decimal>(scaleMin, scaleMax, step);
}
static void Main()
{
// Dummy code to show a usage example.
var minimumValue = data.Min();
var maximumValue = data.Max();
var results = GetScaleDetails(minimumValue, maximumValue);
chart.YAxis.MinValue = results.Item1;
chart.YAxis.MaxValue = results.Item2;
chart.YAxis.Step = results.Item3;
}
取最长的线段大约为零(或整个图表,如果零不在范围内) - 例如,如果您有范围 [-5, 1] 的内容,则取 [-5,0]。
计算出该段的大约长度(以刻度为单位)。这只是将刻度线的长度除以宽度。因此,假设该方法说我们可以在 -5 到 0 之间放入 11 个刻度。这是我们的上限。对于较短的一侧,我们只需将结果镜像到较长的一侧即可。
现在尝试输入尽可能多的(最多 11 个)刻度,以便每个刻度的标记采用 i*10*10^n、i*5*10^n、i*2*10^n 的形式,其中 n 是整数,i 是刻度的索引。现在这是一个优化问题 - 我们希望最大化可以输入的刻度数,同时最小化最后一个刻度与结果末尾之间的距离。因此,为获得尽可能多的刻度分配一个分数,小于我们的上限,并为使最后一个刻度接近 n 分配一个分数 - 您必须在这里进行实验。
在上面的示例中,尝试 n = 1。我们得到 1 个刻度(在 i=0 时)。 n = 2 给了我们 1 个刻度,我们离下界更远,所以我们知道我们必须走另一条路。 n = 0 为每个整数点提供 6 个刻度。 n = -1 给我们 12 个刻度 (0, -0.5, ..., -5.0)。 n = -2 给我们 24 个刻度,依此类推。评分算法会给他们每个人一个分数 - 越高意味着更好的方法。
对 i * 5 * 10^n 和 i*2*10^n 再次执行此操作,并取得分最高的那个。
(作为评分算法的示例,假设分数是到最后一个刻度的距离乘以最大刻度数减去所需的数量。这可能很糟糕,但它将作为一个不错的起点)。
这个简单的算法产生的间隔是 1、2 或 5 的 10 次幂的倍数。并且轴范围被划分为至少 5 个间隔。代码示例是java语言的:
protected double calculateInterval(double range) {
double x = Math.pow(10.0, Math.floor(Math.log10(range)));
if (range / x >= 5)
return x;
else if (range / (x / 2.0) >= 5)
return x / 2.0;
else
return x / 5.0;
}
这是一种替代方案,至少 10 个间隔:
protected double calculateInterval(double range) {
double x = Math.pow(10.0, Math.floor(Math.log10(range)));
if (range / (x / 2.0) >= 10)
return x / 2.0;
else if (range / (x / 5.0) >= 10)
return x / 5.0;
else
return x / 10.0;
}
我一直在使用 jQuery flot 图形库。它是开源的,并且轴/刻度生成非常好。我建议查看它的代码并从中汲取一些想法。
有趣的是,就在一个多星期前,我来到这里寻找同一问题的答案,但又离开并决定提出自己的算法。我是来分享的,如果有什么用的话。
我用 Python 编写代码是为了尝试尽快找到解决方案,但它可以轻松移植到任何其他语言。
下面的函数计算给定数据范围的适当间隔(我允许为
10**n
、2*10**n
、4*10**n
或 5*10**n
),然后计算放置刻度的位置(基于范围内的哪些数字可以被间隔整除)。我没有使用模 %
运算符,因为由于浮点算术舍入错误,它无法正常处理浮点数。
代码:
import math
def get_tick_positions(data: list):
if len(data) == 0:
return []
retpoints = []
data_range = max(data) - min(data)
lower_bound = min(data) - data_range/10
upper_bound = max(data) + data_range/10
view_range = upper_bound - lower_bound
num = lower_bound
n = math.floor(math.log10(view_range) - 1)
interval = 10**n
num_ticks = 1
while num <= upper_bound:
num += interval
num_ticks += 1
if num_ticks > 10:
if interval == 10 ** n:
interval = 2 * 10 ** n
elif interval == 2 * 10 ** n:
interval = 4 * 10 ** n
elif interval == 4 * 10 ** n:
interval = 5 * 10 ** n
else:
n += 1
interval = 10 ** n
num = lower_bound
num_ticks = 1
if view_range >= 10:
copy_interval = interval
else:
if interval == 10 ** n:
copy_interval = 1
elif interval == 2 * 10 ** n:
copy_interval = 2
elif interval == 4 * 10 ** n:
copy_interval = 4
else:
copy_interval = 5
first_val = 0
prev_val = 0
times = 0
temp_log = math.log10(interval)
if math.isclose(lower_bound, 0):
first_val = 0
elif lower_bound < 0:
if upper_bound < -2*interval:
if n < 0:
copy_ub = round(upper_bound*10**(abs(temp_log) + 1))
times = copy_ub // round(interval*10**(abs(temp_log) + 1)) + 2
else:
times = upper_bound // round(interval) + 2
while first_val >= lower_bound:
prev_val = first_val
first_val = times * copy_interval
if n < 0:
first_val *= (10**n)
times -= 1
first_val = prev_val
times += 3
else:
if lower_bound > 2*interval:
if n < 0:
copy_ub = round(lower_bound*10**(abs(temp_log) + 1))
times = copy_ub // round(interval*10**(abs(temp_log) + 1)) - 2
else:
times = lower_bound // round(interval) - 2
while first_val < lower_bound:
first_val = times*copy_interval
if n < 0:
first_val *= (10**n)
times += 1
if n < 0:
retpoints.append(first_val)
else:
retpoints.append(round(first_val))
val = first_val
times = 1
while val <= upper_bound:
val = first_val + times * interval
if n < 0:
retpoints.append(val)
else:
retpoints.append(round(val))
times += 1
retpoints.pop()
return retpoints
将以下三个数据点传递给函数时
points = [-0.00493, -0.0003892, -0.00003292]
...我得到的输出(作为列表)如下:
[-0.005, -0.004, -0.003, -0.002, -0.001, 0.0]
通过此时:
points = [1.399, 38.23823, 8309.33, 112990.12]
...我明白:
[0, 20000, 40000, 60000, 80000, 100000, 120000]
通过此时:
points = [-54, -32, -19, -17, -13, -11, -8, -4, 12, 15, 68]
...我明白:
[-60, -40, -20, 0, 20, 40, 60, 80]
...这似乎都是放置蜱虫的不错的位置选择。
该函数被编写为允许 5-10 个刻度,但如果您愿意,可以轻松更改。
提供的数据列表是否包含有序或无序数据并不重要,因为重要的是列表中的最小和最大数据点。
很好的答案,现在是时间序列......如果我有答案,我会发布。