图上“漂亮”网格线间隔的算法

时间:2008-12-12 01:54:20

标签: algorithm charts graph

我需要一个相当智能的算法来为图表(图表)提供“漂亮”的网格线。

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

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

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

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

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

应该指定范围内的网格线(刻度线)的数量或给定范围内的数字(例如3-8),使得值“很好”(即圆数)并且最大化使用图表区域。在我们的例子中,80将是一个合理的最大值,因为它将使用90%的图表高度(72/80),而100将产生更多的浪费空间。

任何人都知道这个算法很好吗?语言是无关紧要的,因为我将按照我的需要实现它。

16 个答案:

答案 0 :(得分: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

答案 1 :(得分:29)

问题有2件:

  1. 确定所涉及的数量级,
  2. 回合方便。
  3. 您可以使用对数处理第一部分:

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

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

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

    value_per_division = magnitude / subdivisions;
    

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

答案 2 :(得分:14)

我使用以下算法。它与此处发布的其他内容类似,但它是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;
    }
}

答案 3 :(得分:8)

CPAN提供了一个实现here(参见源链接)

另请参阅 Tickmark algorithm for a graph axis

仅供参考,包含您的样本数据:

  • Maple:Min = 8,Max = 74,Labels = 10,20,..,60,70,Ticks = 10,12,14,.. 70,72
  • MATLAB:Min = 10,Max = 80,Labels = 10,20 ,, ..,60,80

答案 4 :(得分:5)

这是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;
};

答案 5 :(得分: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)));

代码的第一部分基于我发现的计算here来计算漂亮的图轴刻度和类似于excel图的刻度。它适用于所有类型的数据集。以下是iPhone实施的示例:

enter image description here

答案 6 :(得分:2)

取自上面的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);
    }

}
With ability to create an instance ,but if you just want calculate and throw it away:   
double tickX = new AxisAssists(aMaxX - aMinX, 8).Tick;

答案 7 :(得分:1)

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

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

至于选择刻度线间距,我建议任意数量的形式10 ^ x * i / n,其中i

答案 8 :(得分:1)

我是“Algorithm for Optimal Scaling on a Chart Axis”的作者。它曾经托管在trollop.org上,但我最近移动了域/博客引擎。

请参阅我的answer to a related question

答案 9 :(得分:0)

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

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;
}

答案 10 :(得分: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)
}

其中range参数是domain的max-min。

答案 11 :(得分:0)

这是我写的一个javascript函数,用于将网格间隔(max-min)/gridLinesNumber舍入为漂亮的值。它可以与任何数字一起使用,请参阅gist并详细说明它是如何工作的,以及如何调用它。

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

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

答案 12 :(得分:0)

如果你试图在VB.NET图表上看到正确的比例,那么我已经使用了Adam Liss的例子,但是确保当你设置从变量传递它们的最小和最大比例值时十进制类型(不是单一或双重类型),否则刻度标记值最终设置为8个小数位。 举个例子,我有一个图表,我将最小Y轴值设置为0.0001,最大Y轴值设置为0.002。 如果我将这些值作为单个参数传递给图表对象,我会得到刻度线值0.00048000001697801,0.000860000036482233 .... 然而,如果我将这些值作为小数传递给图表对象,我会得到漂亮的刻度标记值0.00048,0.00086 ......

答案 13 :(得分:0)

在python中:

steps = [numpy.round(x) for x in np.linspace(min, max, num=num_of_steps)]

答案 14 :(得分:0)

可以始终始终绘制0的答案,可以处理正数和负数以及小数和大数,给出刻度间隔的大小以及要绘制的数量;用Go语言编写

forcePlotZero更改了四舍五入最大值的方式,因此它将始终是一个很好的倍数,然后返回零。示例:

如果forcePlotZero == false,则为237-> 240

如果forcePlotZero == true,则为237-> 300

间隔的计算方法是:获取最大值的10/100/1000等的倍数,然后减去直到这些减法的累计总和为

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

<身体>
强制绘制零 最大和最小输入 最大和最小取整 间隔
forcePlotZero = false 最小值:-104最大值:240 最小值:-160最大值:240 intervalCount:5 intervalSize:100
forcePlotZero = true 最小值:-104最大值:240 最小值:-200最大值:300 intervalCount:6 intervalSize:100
forcePlotZero = false 最小值:40最大值:1240 最小值:0最大值:1300 intervalCount:14 intervalSize:100
forcePlotZero = false 最小值:200最大值:240 最小值:190最大值:240 intervalCount:6 intervalSize:10
forcePlotZero = false 最小值:0.7最大值:1.12 最小值:0.6最大值:1.2 intervalCount:7 intervalSize:0.1
forcePlotZero = false 最低:-70.5最高:-12.5 记住:-80最大:-10 intervalCount:8 intervalSize: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}

答案 15 :(得分:0)

这是在 python 中,用于 base 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