我正在寻找一种算法,用于在一个轴上放置刻度线,需要提供要显示的范围、要显示宽度以及测量刻度线字符串宽度的函数。
例如,假设需要显示1e-6到5e-6之间的数字,并提供一个要显示的像素宽度,该算法将确定应在1e-6、2e-6、3e-6、4e-6和5e-6处放置刻度线。当提供的宽度较小时,算法可能会决定最佳位置仅为偶数位置,即2e-6和4e-6(因为添加更多的刻度线会导致它们重叠)。
智能的算法会优先考虑10、5和2的倍数刻度线,并且在零点周围对称。
我正在寻找一种算法,用于在一个轴上放置刻度线,需要提供要显示的范围、要显示宽度以及测量刻度线字符串宽度的函数。
例如,假设需要显示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;
}
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个刻度,但如果您愿意,可以轻松更改。
无论提供的数据列表是有序还是无序的,都没有关系,因为只有列表中的最小值和最大值才重要。
我一直在使用jQuery flot图形库。它是开源的,并且可以很好地生成坐标轴/刻度。我建议您查看它的代码并从中借鉴一些想法。
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;
}
scaleMax
是0.6
。这避免了极端值恰好在轴限制上的情况。为了避免出现奇怪的步长或范围过大,妥协的办法是减少步数。请注意注释,其中写着“目标在 Y 轴上显示的值的数量 (可能更少)”。 如果您想要确切的 8 步数,您必须选择一个更丑陋的步长或者更宽的轴范围,而这种逻辑认为这些都是更糟糕的选项。 您可以尝试添加更多的值到goodNormalizedSteps
中,例如0.3
、0.8
。 - AndrewgoodNormalizedSteps
加上3.15m
或3.2m
才能恰好达到8个步骤。 - Andrew3.33
,最好像这样:...2.5m, 10/3m, 5, ...
。你将得到从0.3
到0.5666
的 8 个步骤。 - Andrew