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

我需要一个相当聪明的算法来想出一个“好”的网格线图(图表)。

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

最小值: 10 最大值: 72 射程: 62

第一个问题是: 你从哪里开始?在这种情况下,直观的值应该是0,但是这个值在其他数据集上是不成立的,所以我猜测:

网格最小值应该是0或者比范围内数据的最小值小的“漂亮”值。或者,也可以指定。

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

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

有人知道一个好的算法吗?语言是无关紧要的,因为我将在我需要的时候实现它。

42890 次浏览

我用了一种蛮力的方法。首先,计算出你可以容纳的最大刻度数。用刻度数除以值的总范围,这是刻度的 最低限度间距。现在计算以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. 确定所涉及的数量级,以及
  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()例如:。

CPAN 提供了一个实现 给你(参见源代码链接)

参见 图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴标记算法图轴

仅供参考,你的样本数据:

  • 枫树: 最小 = 8,最大 = 74,标签 = 10,20,. . ,60,70,刻度 = 10,12,14,. . 70,72
  • MATLAB: 最小 = 10,最大 = 80,标签 = 10,20,... ,60,80

Another idea is to have the range of the axis be the range of the values, but put the tick marks at the appropriate position.. i.e. for 7 to 22 do:

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

至于选择刻度间距,我建议使用格式10 ^ x * i/n 中的任意数字,其中 i < n 和0 < n < 10。生成这个列表并对它们进行排序,您可以使用二进制搜索找到小于 value _ per _ Division (如 adam _ liss)的最大数。

我使用下面的算法,它和这里发布的其他算法类似,但是它是 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;
}
}

下面是 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;
};

我是“ 图轴最优缩放算法”的作者,它以前是托管在 trolop.org 上的,但我最近移动了域名/博客引擎。

请看我的 回答相关问题

从这里已经有的答案中获得了很多灵感,下面是我在 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;
}

在 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)
}

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

我编写了一个 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 实现的例子:

enter image description here

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

下面是我写的一个 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

检查 小提琴来实验代码。答案中的代码,大意和小提琴稍有不同,我用的是答案中给出的那个。

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

在巨蟒中:

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

答案可以动态地总是绘制0 ,处理正数和负数,以及大小数字,给出蜱虫间隔的大小以及要绘制的数量; 用 Go 编写

ForcPlotZero 改变了最大值的四舍五入方式,因此它总是能得到一个很好的倍数,然后返回到零。例如:

如果 forcPlotZero = = false,则为237—— > 240

如果 forcPlotZero = = true 那么237—— > 300

区间计算得到的倍数为10/100/1000等最大,然后减去,直到这些减去的累计总数 < min

下面是函数的输出,同时显示了 forcPlotZero

力量到零点 最大和最小输入 四舍五入的最大值和最小值 间隔
= false 分钟: -104最大: 240 - 160最大: 240 IntervalCount: 5 intervalSize: 100
= 真 分钟: -104最大: 240 去雷: -200最大: 300 IntervalCount: 6 intervalSize: 100
= false 分钟: 40最多: 1240 最多1300 IntervalCount: 14 intervalSize: 100
= false 分钟: 200最多: 240 最多190最多240 IntervalCount: 6 intervalSize: 10
= false 最小: 0.7最大: 1.12 最多0.6最多1.2 IntervalCount: 7 intervalSize: 0.1
= false 最小: -70.5 max: -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}

这是在 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