图轴的刻度标记算法

问题描述 投票:0回答:6

我正在寻找一种将刻度线放置在轴上的算法,给定显示范围、显示宽度以及测量刻度线字符串宽度的函数。

例如,考虑到我需要在 1e-6 和 5e-6 之间显示以及以像素为单位显示的宽度,算法将确定我应该在 1e-6、2e-6、3e- 处放置刻度线(例如) 6、4e-6和5e-6。给定较小的宽度,它可能会决定最佳放置仅在偶数位置,即 2e-6 和 4e-6(因为放置更多刻度线会导致它们重叠)。

智能算法会优先考虑 10、5 和 2 倍数的刻度线。此外,智能算法会围绕零对称。

algorithm optimization paint
6个回答
16
投票

由于我不喜欢迄今为止找到的任何解决方案,因此我实现了自己的解决方案。它是 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;
}

2
投票

取最长的线段大约为零(或整个图表,如果零不在范围内) - 例如,如果您有范围 [-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
投票

这个简单的算法产生的间隔是 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;
}

1
投票

我一直在使用 jQuery flot 图形库。它是开源的,并且轴/刻度生成非常好。我建议查看它的代码并从中汲取一些想法。


1
投票

有趣的是,就在一个多星期前,我来到这里寻找同一问题的答案,但又离开并决定提出自己的算法。我是来分享的,如果有什么用的话。

我用 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 个刻度,但如果您愿意,可以轻松更改。

提供的数据列表是否包含有序或无序数据并不重要,因为重要的是列表中的最小和最大数据点。


0
投票

很好的答案,现在是时间序列......如果我有答案,我会发布。

© www.soinside.com 2019 - 2024. All rights reserved.