web-dev-qa-db-ja.com

グラフ上の「良い」グリッド線間隔のアルゴリズム

グラフ(グラフ)の「素敵な」グリッド線を作成するには、かなりスマートなアルゴリズムが必要です。

たとえば、値が10、30、72、および60の棒グラフを想定します。

最小値:10最大値:72範囲:62

最初の質問は:何から始めますか?この場合、0は直観的な値ですが、これは他のデータセットでは保持されないため、推測しています。

グリッドの最小値は、0または範囲内のデータの最小値よりも低い「ナイス」値のいずれかでなければなりません。または、指定することもできます。

グリッドの最大値は、範囲内の最大値を超える「ナイス」値である必要があります。または、指定することもできます(たとえば、実際の値に関係なく、パーセンテージを表示する場合は0〜100にすることができます)。

範囲内のグリッド線(目盛り)の数は、値が "いい"(つまり、丸い数字)になり、チャート領域の使用を最大化するように、指定された範囲内の数(3-8など)を指定する必要があります。この例では、80はチャートの高さの90%(72/80)を使用するため、80が賢明な最大値になり、100は無駄なスペースをより多く作成します。

誰でもこれに適したアルゴリズムを知っていますか?言語は私が必要なものに実装するので、無関係です。

61
cletus

CPANは実装を提供します here (ソースリンクを参照)

グラフ軸の目盛りアルゴリズムも参照してください

参考までに、サンプルデータを使用します。

  • メープル:最小= 8、最大= 74、ラベル= 10、20、..、60、70、ティック= 10、12、14、.. 70、72
  • MATLAB:最小= 10、最大= 80、ラベル= 10、20、..、60、80
8

私はこれを一種のブルートフォースメソッドで実行しました。まず、スペースに収まる目盛りの最大数を計算します。値の合計範囲をティック数で割ります。これは 最小 目盛りの間隔。ここで、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倍になる可能性があるため、1人のコメンターが提供された選択に不満を感じているようです。 Nice間隔のテーブルを定義するわずかな変更を以下に示します。この例では、目盛りの数が最大値の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
34
Mark Ransom

問題には2つの部分があります。

  1. 関係する規模の順序を決定し、
  2. 便利なものに丸めます。

対数を使用して最初の部分を処理できます。

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

したがって、たとえば、範囲が50〜1200の場合、指数は3で、大きさは1000です。

次に、グリッドにいくつのサブディビジョンを含めるかを決定して、2番目の部分を処理します。

_value_per_division = magnitude / subdivisions;
_

指数が整数に切り捨てられているため、これは大まかな計算です。境界条件をより適切に処理するために指数計算を微調整することができます。e.g。細分化が多すぎる場合は、int()の代わりに丸めることにより.

29
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;
    }
}
14
Drew Noakes

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
Drew Noakes

上記の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;
2
Gregor Schmitz

データセットの指定された最小値と最大値に対してNice軸スケールとNiceティックを返す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

2
JFS

私は「 チャート軸の最適スケーリングのアルゴリズム 」の著者です。以前はtrollop.orgでホストされていましたが、最近ドメイン/ブログエンジンを移動しました。

関連する質問への回答 をご覧ください。

1
Incongruous

別のアイデアは、軸の範囲を値の範囲にすることですが、目盛りを適切な位置に配置することです。つまり、7から22の場合:

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

ティック間隔の選択に関しては、10 ^ x * i/nの形式の任意の数をお勧めします。ここで、i <nおよび0 <n <10です。このリストを生成し、並べ替えると、最大数を見つけることができますバイナリ検索を使用したvalue_per_division(adam_lissなど)よりも小さい。

1
FryGuy

Pythonの場合:

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

VB.NETチャートでスケールを正しく表示しようとしている場合、Adam Lissの例を使用しましたが、decimal型の変数から渡す最小スケール値と最大スケール値を設定するときは確認してください(タイプがsingleまたはdoubleではありません)そうでない場合、目盛り値は小数点以下8桁のように設定されます。そのため、例として、最小Y軸の値を0.0001に、最大Y軸の値を0.002に設定した1つのチャートがありました。これらの値をシングルとしてチャートオブジェクトに渡すと、0.00048000001697801、0.000860000036482233の目盛り値を取得します。一方、これらの値を小数としてチャートオブジェクトに渡すと、0.00048、0.00086の素敵な目盛り値を取得します。 ..

0
Kristian

ここですでに利用可能な回答から多くのインスピレーションを使用して、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;
}
0
Gleno

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

ここで、範囲の引数はドメインの最大値です。

0
Museful

以下は、グリッド間隔_(max-min)/gridLinesNumber_を美しい値に丸めるために書いたJavaScript関数です。どんな数字でも動作します。詳細なコメットで 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 をチェックして、コードを試してください。答えのコード、要点とフィドルは少し異なります。私は答えで与えられたものを使用しています。

0
grabantot