69

我需要一个相当聪明的算法来为图形(图表)提出“漂亮”的网格线。

例如,假设一个条形图的值为 10、30、72 和 60。您知道:

最小值:10 最大值:72 范围:62

第一个问题是:你从什么开始?在这种情况下,0 将是直观的值,但这不适用于其他数据集,所以我猜:

网格最小值应为 0 或低于范围内数据最小值的“好”值。或者,可以指定它。

网格最大值应该是高于该范围最大值的“好”值。或者,可以指定它(例如,如果您显示百分比,则可能需要 0 到 100,而与实际值无关)。

范围内的网格线(刻度线)的数量应该是指定的,或者是给定范围内的一个数字(例如 3-8),这样值是“不错的”(即整数),并且您可以最大限度地利用图表区域。在我们的示例中,80 将是一个合理的最大值,因为这将使用图表高度的 90% (72/80),而 100 会浪费更多空间。

任何人都知道一个好的算法吗?语言是无关紧要的,因为我会在我需要的地方实现它。

4

16 回答 16

34

我已经用一种蛮力方法做到了这一点。首先,找出你可以放入空间的最大刻度线数。将值的总范围除以刻度数;这是刻度线的最小间距。现在计算以 10 为底的对数下限以获得刻度的大小,然后除以该值。您最终应该得到 1 到 10 范围内的值。只需选择大于或等于该值的整数并将其乘以之前计算的对数即可。这是您的最终刻度间距。

Python 中的示例:

import math

def BestTick(largest, mostticks):
    minimum = largest / mostticks
    magnitude = 10 ** math.floor(math.log(minimum, 10))
    residual = minimum / magnitude
    if residual > 5:
        tick = 10 * magnitude
    elif residual > 2:
        tick = 5 * magnitude
    elif residual > 1:
        tick = 2 * magnitude
    else:
        tick = magnitude
    return tick

编辑:您可以自由更改“好”间隔的选择。一位评论者似乎对提供的选择不满意,因为实际的刻度数可能比最大值少 2.5 倍。这是一个小的修改,它定义了一个漂亮的间隔表。在示例中,我扩展了选择,以便刻度数不会小于最大值的 3/5。

import bisect

def BestTick2(largest, mostticks):
    minimum = largest / mostticks
    magnitude = 10 ** math.floor(math.log(minimum, 10))
    residual = minimum / magnitude
    # this table must begin with 1 and end with 10
    table = [1, 1.5, 2, 3, 5, 7, 10]
    tick = table[bisect.bisect_right(table, residual)] if residual < 10 else 10
    return tick * magnitude
于 2008-12-12T02:02:13.560 回答
30

有2个问题:

  1. 确定所涉及的数量级,以及
  2. 四舍五入到方便的地方。

您可以使用对数处理第一部分:

range = max - min;  
exponent = int(log(range));       // See comment below.
magnitude = pow(10, exponent);

因此,例如,如果您的范围是 50 - 1200,则指数为 3,幅度为 1000。

然后通过确定网格中需要多少细分来处理第二部分:

value_per_division = magnitude / subdivisions;

这是一个粗略的计算,因为指数已被截断为整数。您可能想要调整指数计算以更好地处理边界条件,例如通过舍入而不是采用int()如果最终细分太多。

于 2008-12-12T02:09:48.630 回答
17

我使用以下算法。它与此处发布的其他人相似,但它是 C# 中的第一个示例。

public static class AxisUtil
{
    public static float CalcStepSize(float range, float targetSteps)
    {
        // calculate an initial guess at step size
        var tempStep = range/targetSteps;

        // get the magnitude of the step size
        var mag = (float)Math.Floor(Math.Log10(tempStep));
        var magPow = (float)Math.Pow(10, mag);

        // calculate most significant digit of the new step size
        var magMsd = (int)(tempStep/magPow + 0.5);

        // promote the MSD to either 1, 2, or 5
        if (magMsd > 5)
            magMsd = 10;
        else if (magMsd > 2)
            magMsd = 5;
        else if (magMsd > 1)
            magMsd = 2;

        return magMsd*magPow;
    }
}
于 2009-02-13T13:41:02.147 回答
11

CPAN在这里提供了一个实现(见源链接)

另请参见图轴的 Tickmark 算法

仅供参考,使用您的示例数据:

  • 枫木:Min=8,Max=74,Labels=10,20,..,60,70,Ticks=10,12,14,..70,72
  • MATLAB:最小值=10,最大值=80,标签=10,20,,..,60,80
于 2008-12-12T02:19:55.147 回答
6

这是 JavaScript 中的另一个实现:

var calcStepSize = function(range, targetSteps)
{
  // calculate an initial guess at step size
  var tempStep = range / targetSteps;

  // get the magnitude of the step size
  var mag = Math.floor(Math.log(tempStep) / Math.LN10);
  var magPow = Math.pow(10, mag);

  // calculate most significant digit of the new step size
  var magMsd = Math.round(tempStep / magPow + 0.5);

  // promote the MSD to either 1, 2, or 5
  if (magMsd > 5.0)
    magMsd = 10.0;
  else if (magMsd > 2.0)
    magMsd = 5.0;
  else if (magMsd > 1.0)
    magMsd = 2.0;

  return magMsd * magPow;
};
于 2013-02-25T16:47:32.610 回答
3

取自上面的 Mark,一个稍微完整的 C# 中的 Util 类。这也计算了一个合适的第一个和最后一个刻度。

public  class AxisAssists
{
    public double Tick { get; private set; }

    public AxisAssists(double aTick)
    {
        Tick = aTick;
    }
    public AxisAssists(double range, int mostticks)
    {
        var minimum = range / mostticks;
        var magnitude = Math.Pow(10.0, (Math.Floor(Math.Log(minimum) / Math.Log(10))));
        var residual = minimum / magnitude;
        if (residual > 5)
        {
            Tick = 10 * magnitude;
        }
        else if (residual > 2)
        {
            Tick = 5 * magnitude;
        }
        else if (residual > 1)
        {
            Tick = 2 * magnitude;
        }
        else
        {
            Tick = magnitude;
        }
    }

    public double GetClosestTickBelow(double v)
    {
        return Tick* Math.Floor(v / Tick);
    }
    public double GetClosestTickAbove(double v)
    {
        return Tick * Math.Ceiling(v / Tick);
    }
}

具有创建实例的能力,但如果您只想计算并丢弃它:

    double tickX = new AxisAssists(aMaxX - aMinX, 8).Tick;
于 2014-10-01T16:36:05.013 回答
2

我是“图表轴上的最佳缩放算法”的作者。它曾经托管在 trollop.org 上,但我最近移动了域/博客引擎。

请参阅我对相关问题的回答

于 2013-05-03T16:16:21.003 回答
2

我写了一个objective-c方法来为你的数据集的给定的最小值和最大值返回一个漂亮的轴刻度和漂亮的刻度:

- (NSArray*)niceAxis:(double)minValue :(double)maxValue
{
    double min_ = 0, max_ = 0, min = minValue, max = maxValue, power = 0, factor = 0, tickWidth, minAxisValue = 0, maxAxisValue = 0;
    NSArray *factorArray = [NSArray arrayWithObjects:@"0.0f",@"1.2f",@"2.5f",@"5.0f",@"10.0f",nil];
    NSArray *scalarArray = [NSArray arrayWithObjects:@"0.2f",@"0.2f",@"0.5f",@"1.0f",@"2.0f",nil];

    // calculate x-axis nice scale and ticks
    // 1. min_
    if (min == 0) {
        min_ = 0;
    }
    else if (min > 0) {
        min_ = MAX(0, min-(max-min)/100);
    }
    else {
        min_ = min-(max-min)/100;
    }

    // 2. max_
    if (max == 0) {
        if (min == 0) {
            max_ = 1;
        }
        else {
            max_ = 0;
        }
    }
    else if (max < 0) {
        max_ = MIN(0, max+(max-min)/100);
    }
    else {
        max_ = max+(max-min)/100;
    }

    // 3. power
    power = log(max_ - min_) / log(10);

    // 4. factor
    factor = pow(10, power - floor(power));

    // 5. nice ticks
    for (NSInteger i = 0; factor > [[factorArray objectAtIndex:i]doubleValue] ; i++) {
        tickWidth = [[scalarArray objectAtIndex:i]doubleValue] * pow(10, floor(power));
    }

    // 6. min-axisValues
    minAxisValue = tickWidth * floor(min_/tickWidth);

    // 7. min-axisValues
    maxAxisValue = tickWidth * floor((max_/tickWidth)+1);

    // 8. create NSArray to return
    NSArray *niceAxisValues = [NSArray arrayWithObjects:[NSNumber numberWithDouble:minAxisValue], [NSNumber numberWithDouble:maxAxisValue],[NSNumber numberWithDouble:tickWidth], nil];

    return niceAxisValues;
}

您可以像这样调用该方法:

NSArray *niceYAxisValues = [self niceAxis:-maxy :maxy];

并让您设置轴:

double minYAxisValue = [[niceYAxisValues objectAtIndex:0]doubleValue];
double maxYAxisValue = [[niceYAxisValues objectAtIndex:1]doubleValue];
double ticksYAxis = [[niceYAxisValues objectAtIndex:2]doubleValue];

以防万一您想限制轴刻度的数量,请执行以下操作:

NSInteger maxNumberOfTicks = 9;
NSInteger numberOfTicks = valueXRange / ticksXAxis;
NSInteger newNumberOfTicks = floor(numberOfTicks / (1 + floor(numberOfTicks/(maxNumberOfTicks+0.5))));
double newTicksXAxis = ticksXAxis * (1 + floor(numberOfTicks/(maxNumberOfTicks+0.5)));

代码的第一部分基于我在这里找到的计算来计算漂亮的图形轴比例和类似于 excel 图形的刻度。它适用于所有类型的数据集。下面是一个 iPhone 实现的例子:

在此处输入图像描述

于 2014-04-03T20:33:27.917 回答
1

另一个想法是让轴的范围是值的范围,但将刻度线放在适当的位置..即7到22:

[- - - | - - - - | - - - - | - - ]
       10 15 20

至于选择刻度间距,我建议使用 10^x * i / n 形式的任意数字,其中 i < n 且 0 < n < 10。生成此列表,并对它们进行排序,您可以找到最大的数字使用二分搜索小于 value_per_division(如在 adam_liss 中)。

于 2008-12-12T02:22:42.473 回答
1

ndex使用从这里已经可用的答案中获得的很多灵感,这是我在 C 中的实现。请注意,数组中内置了一些可扩展性。

float findNiceDelta(float maxvalue, int count)
{
    float step = maxvalue/count,
         order = powf(10, floorf(log10(step))),
         delta = (int)(step/order + 0.5);

    static float ndex[] = {1, 1.5, 2, 2.5, 5, 10};
    static int ndexLenght = sizeof(ndex)/sizeof(float);
    for(int i = ndexLenght - 2; i > 0; --i)
        if(delta > ndex[i]) return ndex[i + 1] * order;
    return delta*order;
}
于 2013-08-05T01:38:10.207 回答
0

在 R 中,使用

tickSize <- function(range,minCount){
    logMaxTick <- log10(range/minCount)
    exponent <- floor(logMaxTick)
    mantissa <- 10^(logMaxTick-exponent)
    af <- c(1,2,5) # allowed factors
    mantissa <- af[findInterval(mantissa,af)]
    return(mantissa*10^exponent)
}

其中范围参数是域的最大最小值。

于 2013-10-04T12:57:10.290 回答
0

这是我编写的一个 javascript 函数,用于将网格间隔舍(max-min)/gridLinesNumber入到漂亮的值。它适用于任何数字,请参阅带有详细注释的要点以了解它的工作原理以及如何调用它。

var ceilAbs = function(num, to, bias) {
  if (to == undefined) to = [-2, -5, -10]
  if (bias == undefined) bias = 0
  var numAbs = Math.abs(num) - bias
  var exp = Math.floor( Math.log10(numAbs) )

    if (typeof to == 'number') {
        return Math.sign(num) * to * Math.ceil(numAbs/to) + bias
    }

  var mults = to.filter(function(value) {return value > 0})
  to = to.filter(function(value) {return value < 0}).map(Math.abs)
  var m = Math.abs(numAbs) * Math.pow(10, -exp)
  var mRounded = Infinity

  for (var i=0; i<mults.length; i++) {
    var candidate = mults[i] * Math.ceil(m / mults[i])
    if (candidate < mRounded)
      mRounded = candidate
  }
  for (var i=0; i<to.length; i++) {
    if (to[i] >= m && to[i] < mRounded)
      mRounded = to[i]
  }
  return Math.sign(num) * mRounded * Math.pow(10, exp) + bias
}

调用ceilAbs(number, [0.5])不同的数字将像这样对数字进行四舍五入:

301573431.1193228 -> 350000000
14127.786597236991 -> 15000
-63105746.17236853 -> -65000000
-718854.2201183736 -> -750000
-700660.340487957 -> -750000
0.055717507097870114 -> 0.06
0.0008068701205775142 -> 0.00085
-8.66660070605576 -> -9
-400.09256079792976 -> -450
0.0011740548815578223 -> 0.0015
-5.3003294346854085e-8 -> -6e-8
-0.00005815960629843176 -> -0.00006
-742465964.5184875 -> -750000000
-81289225.90985894 -> -85000000
0.000901771713513881 -> 0.00095
-652726598.5496342 -> -700000000
-0.6498901364393532 -> -0.65
0.9978325804695487 -> 1
5409.4078950583935 -> 5500
26906671.095639467 -> 30000000

查看小提琴以试验代码。答案中的代码,要点和小提琴略有不同,我使用的是答案中给出的代码。

于 2016-02-07T13:22:26.743 回答
0

如果您试图让比例尺在 VB.NET 图表上看起来正确,那么我使用了 Adam Liss 的示例,但请确保在设置最小和最大比例尺值时将它们从十进制类型的变量中传入(不是单或双类型)否则刻度线值最终设置为小数点后 8 位。例如,我有 1 个图表,其中我将最小 Y 轴值设置为 0.0001,将最大 Y 轴值设置为 0.002。如果我将这些值作为单曲传递给图表对象,我会得到 0.00048000001697801、0.000860000036482233 的刻度线值 .... 而如果我将这些值作为小数传递给图表对象,我会得到很好的刻度线值 0.00048、0.00086 .... ..

于 2016-04-08T12:44:37.270 回答
0

在蟒蛇中:

steps = [numpy.round(x) for x in np.linspace(min, max, num=num_of_steps)]
于 2018-01-01T20:29:30.317 回答
0

可以动态始终绘制 0 的答案,处理正数和负数以及小数和大数,给出刻度间隔大小和要绘制的数量;用围棋写的

forcePlotZero 更改最大值的四舍五入方式,因此它总是会产生一个很好的倍数,然后再回到零。例子:

如果 forcePlotZero == false 那么 237 --> 240

如果 forcePlotZero == true 那么 237 --> 300

间隔的计算方法是将 10/100/1000 等的倍数作为最大值,然后减去,直到这些减法的累积总和小于最小值

这是函数的输出,以及显示 forcePlotZero

强制绘制零 最大和最小输入 舍入最大值和最小值 间隔
forcePlotZero=false 最小值:-104 最大值:240 最小:-160 最大:240 间隔计数:5 间隔大小:100
forcePlotZero=true 最小值:-104 最大值:240 最小:-200 最大:300 间隔计数:6 间隔大小:100
forcePlotZero=false 最小:40 最大:1240 最小:0 最大:1300 间隔计数:14 间隔大小:100
forcePlotZero=false 最小:200 最大:240 最小:190 最大:240 间隔计数:6 间隔大小:10
forcePlotZero=false 最小:0.7 最大:1.12 最小:0.6 最大:1.2 间隔计数:7 间隔大小:0.1
forcePlotZero=false 最小值:-70.5 最大值:-12.5 最小:-80 最大:-10 间隔计数:8 间隔大小:10

这是游乐场链接https://play.golang.org/p/1IhiX_hRQvo

func getMaxMinIntervals(max float64, min float64, forcePlotZero bool) (maxRounded float64, minRounded float64, intervalCount float64, intervalSize float64) {

//STEP 1: start off determining the maxRounded value for the axis
precision := 0.0
precisionDampener := 0.0 //adjusts to prevent 235 going to 300, instead dampens the scaling to get 240
epsilon := 0.0000001
if math.Abs(max) >= 0 && math.Abs(max) < 2 {
    precision = math.Floor(-math.Log10(epsilon + math.Abs(max) - math.Floor(math.Abs(max)))) //counting number of zeros between decimal point and rightward digits
    precisionDampener = 1
    precision = precision + precisionDampener
} else if math.Abs(max) >= 2 && math.Abs(max) < 100 {
    precision = math.Ceil(math.Log10(math.Abs(max)+1)) * -1 //else count number of digits before decimal point
    precisionDampener = 1
    precision = precision + precisionDampener
} else {
    precision = math.Ceil(math.Log10(math.Abs(max)+1)) * -1 //else count number of digits before decimal point
    precisionDampener = 2
    if forcePlotZero == true {
        precisionDampener = 1
    }
    precision = precision + precisionDampener
}

useThisFactorForIntervalCalculation := 0.0 // this is needed because intervals are calculated from the max value with a zero origin, this uses range for min - max
if max < 0 {
    maxRounded = (math.Floor(math.Abs(max)*(math.Pow10(int(precision)))) / math.Pow10(int(precision)) * -1)
    useThisFactorForIntervalCalculation = (math.Floor(math.Abs(max)*(math.Pow10(int(precision)))) / math.Pow10(int(precision))) + ((math.Ceil(math.Abs(min)*(math.Pow10(int(precision)))) / math.Pow10(int(precision))) * -1)
} else {
    maxRounded = math.Ceil(max*(math.Pow10(int(precision)))) / math.Pow10(int(precision))
    useThisFactorForIntervalCalculation = maxRounded
}

minNumberOfIntervals := 2.0
maxNumberOfIntervals := 19.0
intervalSize = 0.001
intervalCount = minNumberOfIntervals

//STEP 2: get interval size (the step size on the axis)
for {
    if math.Abs(useThisFactorForIntervalCalculation)/intervalSize < minNumberOfIntervals || math.Abs(useThisFactorForIntervalCalculation)/intervalSize > maxNumberOfIntervals {
        intervalSize = intervalSize * 10
    } else {
        break
    }
}

//STEP 3: check that intervals are not too large, safety for max and min values that are close together (240, 220 etc)
for {
    if max-min < intervalSize {
        intervalSize = intervalSize / 10
    } else {
        break
    }
}

//STEP 4: now we can get minRounded by adding the interval size to 0 till we get to the point where another increment would make cumulative increments > min, opposite for negative in
minRounded = 0.0

if min >= 0 {
    for {
        if minRounded < min {
            minRounded = minRounded + intervalSize
        } else {
            minRounded = minRounded - intervalSize
            break
        }
    }
} else {
    minRounded = maxRounded //keep going down, decreasing by the interval size till minRounded < min
    for {
        if minRounded > min {
            minRounded = minRounded - intervalSize

        } else {
            break
        }
    }
}

//STEP 5: get number of intervals to draw
intervalCount = (maxRounded - minRounded) / intervalSize
intervalCount = math.Ceil(intervalCount) + 1 // include the origin as an interval

//STEP 6: Check that the intervalCount isn't too high
if intervalCount-1 >= (intervalSize * 2) && intervalCount > maxNumberOfIntervals {
    intervalCount = math.Ceil(intervalCount / 2)
    intervalSize *= 2
}

return}
于 2020-12-11T14:33:07.540 回答
0

这是在 python 中,以 10 为基础。不涵盖您的所有问题,但我认为您可以在此基础上进行构建

import numpy as np

def create_ticks(lo,hi):
    s = 10**(np.floor(np.log10(hi - lo)))
    start = s * np.floor(lo / s)
    end = s * np.ceil(hi / s)
    ticks = [start]
    t = start
    while (t <  end):
        ticks += [t]
        t = t + s
        
    return ticks
于 2021-04-26T14:11:36.353 回答