web-dev-qa-db-ja.com

グラフのY軸の魅力的な線形スケールを選択する

私たちのソフトウェアで棒グラフ(または線グラフ)を表示するコードを少し書いています。すべて順調です。私が困惑しているのは、Y軸にラベルを付けることです。

呼び出し元は、Yスケールにラベルをどの程度細かく付けたいかを教えてくれますが、「魅力的な」方法でラベルを付けるものに正確にこだわっているようです。私は「魅力的」とは言えませんし、おそらくあなたもそうではありませんが、私たちはそれを見るとわかりますよね?

したがって、データポイントが次の場合:

   15, 234, 140, 65, 90

そして、ユーザーはY軸に10個のラベルを要求します。紙と鉛筆を少し使いこなすと、次のようになります。

  0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250

したがって、そこには10(0を含まない)があり、最後の値は最高値(234 <250)を超えて拡張され、それぞれ25の「ナイス」増分です。 8つのラベルを要求した場合、30の増分はニースに見えます。

  0, 30, 60, 90, 120, 150, 180, 210, 240

9つはトリッキーだったでしょう。たぶん、8または10のいずれかを使用して、それを十分に近く呼び出しても大丈夫でしょう。そして、いくつかのポイントが負の場合はどうしますか?

Excelがこの問題にうまく対処していることがわかります。

誰かがこれを解決するための汎用アルゴリズムを知っていますか(ブルートフォースでも大丈夫です)?すぐにやる必要はありませんが、見た目がいいはずです。

71
Clinton Pierce

ずっと前に、私はこれをうまくカバーするグラフモジュールを書きました。灰色の塊を掘ると、次のものが得られます。

  • データの下限と上限を決定します。 (下限=上限の特別な場合に注意してください!
  • 範囲を必要なティック数に分割します。
  • ティックの範囲をニースの金額に切り上げます。
  • それに応じて下限と上限を調整します。

あなたの例を見てみましょう:

15, 234, 140, 65, 90 with 10 ticks
  1. 下限= 15
  2. 上限= 234
  3. 範囲= 234-15 = 219
  4. ティック範囲= 21.9。これは25.0でなければなりません
  5. 新しい下限= 25 * round(15/25)= 0
  6. 新しい上限= 25 * round(1 + 235/25)= 250

したがって、範囲= 0,25,50、...、225,250

次の手順でニースのティック範囲を取得できます。

  1. 結果が0.1〜1.0(1を除く0.1を含む)になるように10 ^ xで除算します。
  2. それに応じて翻訳:
    • 0.1-> 0.1
    • <= 0.2-> 0.2
    • <= 0.25-> 0.25
    • <= 0.3-> 0.3
    • <= 0.4-> 0.4
    • <= 0.5-> 0.5
    • <= 0.6-> 0.6
    • <= 0.7-> 0.7
    • <= 0.75-> 0.75
    • <= 0.8-> 0.8
    • <= 0.9-> 0.9
    • <= 1.0-> 1.0
  3. 10 ^ xを掛けます。

この場合、21.9は10 ^ 2で除算されて0.219になります。これは0.25以下なので、0.25になりました。 10 ^ 2を掛けると、25になります。

8ティックの同じ例を見てみましょう。

15, 234, 140, 65, 90 with 8 ticks
  1. 下限= 15
  2. 上限= 234
  3. 範囲= 234-15 = 219
  4. ティック範囲= 27.375
    1. 0.27375の10 ^ 2で割ると、0.3に変換され、30が得られます(10 ^ 2で乗算)。
  5. 新しい下限= 30 * round(15/30)= 0
  6. 新しい上限= 30 * round(1 + 235/30)= 240

あなたが要求した結果を与える;-)。

------ KDにより追加------

ルックアップテーブルなどを使用せずにこのアルゴリズムを実現するコードを次に示します。

double range = ...;
int tickCount = ...;
double unroundedTickSize = range/(tickCount-1);
double x = Math.ceil(Math.log10(unroundedTickSize)-1);
double pow10x = Math.pow(10, x);
double roundedTickRange = Math.ceil(unroundedTickSize / pow10x) * pow10x;
return roundedTickRange;

一般的に、ティックの数には下のティックが含まれるため、実際のY軸セグメントはティックの数よりも1つ少なくなります。

92
Toon Krijthe

PHP私が使用している例です。この関数は、渡された最小および最大Y値を含むきれいなY軸値の配列を返します。もちろん、このルーチンはXにも使用できます軸の値。

必要なティック数を「提案」できますが、ルーチンは見栄えの良いものを返します。サンプルデータをいくつか追加し、これらの結果を示しました。

#!/usr/bin/php -q
<?php

function makeYaxis($yMin, $yMax, $ticks = 10)
{
  // This routine creates the Y axis values for a graph.
  //
  // Calculate Min AMD Max graphical labels and graph
  // increments.  The number of ticks defaults to
  // 10 which is the SUGGESTED value.  Any tick value
  // entered is used as a suggested value which is
  // adjusted to be a 'pretty' value.
  //
  // Output will be an array of the Y axis values that
  // encompass the Y values.
  $result = array();
  // If yMin and yMax are identical, then
  // adjust the yMin and yMax values to actually
  // make a graph. Also avoids division by zero errors.
  if($yMin == $yMax)
  {
    $yMin = $yMin - 10;   // some small value
    $yMax = $yMax + 10;   // some small value
  }
  // Determine Range
  $range = $yMax - $yMin;
  // Adjust ticks if needed
  if($ticks < 2)
    $ticks = 2;
  else if($ticks > 2)
    $ticks -= 2;
  // Get raw step value
  $tempStep = $range/$ticks;
  // Calculate pretty step value
  $mag = floor(log10($tempStep));
  $magPow = pow(10,$mag);
  $magMsd = (int)($tempStep/$magPow + 0.5);
  $stepSize = $magMsd*$magPow;

  // build Y label array.
  // Lower and upper bounds calculations
  $lb = $stepSize * floor($yMin/$stepSize);
  $ub = $stepSize * ceil(($yMax/$stepSize));
  // Build array
  $val = $lb;
  while(1)
  {
    $result[] = $val;
    $val += $stepSize;
    if($val > $ub)
      break;
  }
  return $result;
}

// Create some sample data for demonstration purposes
$yMin = 60;
$yMax = 330;
$scale =  makeYaxis($yMin, $yMax);
print_r($scale);

$scale = makeYaxis($yMin, $yMax,5);
print_r($scale);

$yMin = 60847326;
$yMax = 73425330;
$scale =  makeYaxis($yMin, $yMax);
print_r($scale);
?>

サンプルデータからの結果出力

# ./test1.php
Array
(
    [0] => 60
    [1] => 90
    [2] => 120
    [3] => 150
    [4] => 180
    [5] => 210
    [6] => 240
    [7] => 270
    [8] => 300
    [9] => 330
)

Array
(
    [0] => 0
    [1] => 90
    [2] => 180
    [3] => 270
    [4] => 360
)

Array
(
    [0] => 60000000
    [1] => 62000000
    [2] => 64000000
    [3] => 66000000
    [4] => 68000000
    [5] => 70000000
    [6] => 72000000
    [7] => 74000000
)
20
Scott Guthrie

このコードを試してください。いくつかのチャート作成シナリオで使用しましたが、うまく機能します。それも非常に高速です。

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

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

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

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

        return magMsd*magPow;
    }
}
8
Drew Noakes

呼び出し元は、必要な範囲を教えてくれないように聞こえます。

したがって、ラベルカウントでうまく分割できるようになるまで、エンドポイントを自由に変更できます。

「いいね」を定義しましょう。ラベルが次のように外れている場合、Niceに電話します。

1. 2^n, for some integer n. eg. ..., .25, .5, 1, 2, 4, 8, 16, ...
2. 10^n, for some integer n. eg. ..., .01, .1, 1, 10, 100
3. n/5 == 0, for some positive integer n, eg, 5, 10, 15, 20, 25, ...
4. n/2 == 0, for some positive integer n, eg, 2, 4, 6, 8, 10, 12, 14, ...

データシリーズの最大値と最小値を見つけます。これらのポイントを呼び出しましょう:

min_point and max_point.

あとは、3つの値を見つけるだけです。

- start_label, where start_label < min_point and start_label is an integer
- end_label, where end_label > max_point and end_label is an integer
- label_offset, where label_offset is "Nice"

方程式に適合します:

(end_label - start_label)/label_offset == label_count

おそらく多くの解決策があるので、1つを選んでください。ほとんどの場合、設定できると思います

start_label to 0

別の整数を試してください

end_label

オフセットが「Nice」になるまで

6
Pyrolistical

私はまだこれと戦っています:)

Gamecatの元の答えはほとんどの場合うまくいくようですが、必要なティック数として「3ティック」をプラグインしてみてください(同じデータ値15、234、140、65、90)。は、73のティック範囲を与えるようです。10^ 2で割った後、0.73になります。これは、0.75にマップされ、75の「ニース」ティック範囲を与えます。

次に、上限の計算:75 * round(1 + 234/75)= 300

および下限:75 * round(15/75)= 0

ただし、0から開始して75のステップで300の上限まで進むと、明らかに0,75,150,225,300 ...になります。これは間違いなく有用ですが、4ティック(0を含まない) 3ティック必要です。

それが100%の時間に機能しないことをただイライラさせます。これはもちろん、どこかで私の間違いに陥る可能性があります。

3
StillPondering

Toon Krijthe による答えは、ほとんどの場合うまくいきます。ただし、過剰な数のティックが生成される場合があります。負の数でも機能しません。問題に対する包括的なアプローチは問題ありませんが、これを処理するより良い方法があります。使用するアルゴリズムは、実際に取得するものによって異なります。以下に、JSプロットライブラリで使用したコードを紹介します。私はそれをテストしましたが、常に動作します(うまくいけば;))。主な手順は次のとおりです。

  • グローバル極値xMinおよびxMaxを取得します(アルゴリズムで印刷するすべてのプロットを含みます)
  • xMinとxMaxの間の範囲を計算する
  • 範囲の大きさのオーダーを計算します
  • 範囲をティック数から1を引いた値で割って、ティックサイズを計算します
  • これはオプションです。常にゼロティックを印刷する場合は、ティックサイズを使用して正および負のティックの数を計算します。ティックの総数は、合計+ 1(ゼロティック)になります
  • 常にゼロティックがある場合、これは必要ありません。下限と上限を計算しますが、プロットの中心を忘れないでください

はじめましょう。まず基本的な計算

    var range = Math.abs(xMax - xMin); //both can be negative
    var rangeOrder = Math.floor(Math.log10(range)) - 1; 
    var power10 = Math.pow(10, rangeOrder);
    var maxRound = (xMax > 0) ? Math.ceil(xMax / power10) : Math.floor(xMax / power10);
    var minRound = (xMin < 0) ? Math.floor(xMin / power10) : Math.ceil(xMin / power10);

プロットがすべてのデータをカバーするように、最小値と最大値を100%に丸めます。範囲のlog10が負であるか否かをフロアし、1を減算することも非常に重要です。そうしないと、アルゴリズムは1未満の数値に対して機能しません。

    var fullRange = Math.abs(maxRound - minRound);
    var tickSize = Math.ceil(fullRange / (this.XTickCount - 1));

    //You can set Nice looking ticks if you want
    //You can find exemplary method below 
    tickSize = this.NiceLookingTick(tickSize);

    //Here you can write a method to determine if you need zero tick
    //You can find exemplary method below
    var isZeroNeeded = this.HasZeroTick(maxRound, minRound, tickSize);

7、13、17などのダニを避けるために「素敵なダニ」を使用します。ここで使用する方法は非常に簡単です。また、必要なときにzeroTickを使用することもできます。このように、プロットははるかにプロフェッショナルに見えます。この回答の最後にすべての方法が記載されています。

次に、上限と下限を計算する必要があります。これはゼロティックで非常に簡単ですが、他の場合にはもう少し努力が必要です。どうして?プロットの上限と下限をうまく中央揃えしたいからです。私のコードを見てください。一部の変数はこのスコープ外で定義され、一部の変数は提示されたコード全体が保持されるオブジェクトのプロパティです。

    if (isZeroNeeded) {

        var positiveTicksCount = 0;
        var negativeTickCount = 0;

        if (maxRound != 0) {

            positiveTicksCount = Math.ceil(maxRound / tickSize);
            XUpperBound = tickSize * positiveTicksCount * power10;
        }

        if (minRound != 0) {
            negativeTickCount = Math.floor(minRound / tickSize);
            XLowerBound = tickSize * negativeTickCount * power10;
        }

        XTickRange = tickSize * power10;
        this.XTickCount = positiveTicksCount - negativeTickCount + 1;
    }
    else {
        var delta = (tickSize * (this.XTickCount - 1) - fullRange) / 2.0;

        if (delta % 1 == 0) {
            XUpperBound = maxRound + delta;
            XLowerBound = minRound - delta;
        }
        else {
            XUpperBound =  maxRound + Math.ceil(delta);
            XLowerBound =  minRound - Math.floor(delta);
        }

        XTickRange = tickSize * power10;
        XUpperBound = XUpperBound * power10;
        XLowerBound = XLowerBound * power10;
    }

そして、あなたが自分で書くことができますが、私のものを使用することもできます

this.NiceLookingTick = function (tickSize) {

    var NiceArray = [1, 2, 2.5, 3, 4, 5, 10];

    var tickOrder = Math.floor(Math.log10(tickSize));
    var power10 = Math.pow(10, tickOrder);
    tickSize = tickSize / power10;

    var niceTick;
    var minDistance = 10;
    var index = 0;

    for (var i = 0; i < NiceArray.length; i++) {
        var dist = Math.abs(NiceArray[i] - tickSize);
        if (dist < minDistance) {
            minDistance = dist;
            index = i;
        }
    }

    return NiceArray[index] * power10;
}

this.HasZeroTick = function (maxRound, minRound, tickSize) {

    if (maxRound * minRound < 0)
    {
        return true;
    }
    else if (Math.abs(maxRound) < tickSize || Math.round(minRound) < tickSize) {

        return true;
    }
    else {

        return false;
    }
}

ここに含まれていないものがもう1つあります。これが「素敵な境界線」です。これらは、「見栄えの良いティック」の数値と同様の数値である下限です。たとえば、同じ目盛りサイズで6から始まるプロットを持つよりも、目盛りサイズ5で5から始まる下限を設定することをお勧めします。しかし、これは解雇されました。

それが役に立てば幸い。乾杯!

3
Arthur

10ステップ+ゼロが必要な場合、これは魅力のように機能します

//get proper scale for y
$maximoyi_temp= max($institucion); //get max value from data array
 for ($i=10; $i< $maximoyi_temp; $i=($i*10)) {   
    if (($divisor = ($maximoyi_temp / $i)) < 2) break; //get which divisor will give a number between 1-2    
 } 
 $factor_d = $maximoyi_temp / $i;
 $factor_d = ceil($factor_d); //round up number to 2
 $maximoyi = $factor_d * $i; //get new max value for y
 if ( ($maximoyi/ $maximoyi_temp) > 2) $maximoyi = $maximoyi /2; //check if max value is too big, then split by 2
1
mario

これを変換 answer asSwift 4

extension Int {

    static func makeYaxis(yMin: Int, yMax: Int, ticks: Int = 10) -> [Int] {
        var yMin = yMin
        var yMax = yMax
        var ticks = ticks
        // This routine creates the Y axis values for a graph.
        //
        // Calculate Min AMD Max graphical labels and graph
        // increments.  The number of ticks defaults to
        // 10 which is the SUGGESTED value.  Any tick value
        // entered is used as a suggested value which is
        // adjusted to be a 'pretty' value.
        //
        // Output will be an array of the Y axis values that
        // encompass the Y values.
        var result = [Int]()
        // If yMin and yMax are identical, then
        // adjust the yMin and yMax values to actually
        // make a graph. Also avoids division by zero errors.
        if yMin == yMax {
            yMin -= ticks   // some small value
            yMax += ticks   // some small value
        }
        // Determine Range
        let range = yMax - yMin
        // Adjust ticks if needed
        if ticks < 2 { ticks = 2 }
        else if ticks > 2 { ticks -= 2 }

        // Get raw step value
        let tempStep: CGFloat = CGFloat(range) / CGFloat(ticks)
        // Calculate pretty step value
        let mag = floor(log10(tempStep))
        let magPow = pow(10,mag)
        let magMsd = Int(tempStep / magPow + 0.5)
        let stepSize = magMsd * Int(magPow)

        // build Y label array.
        // Lower and upper bounds calculations
        let lb = stepSize * Int(yMin/stepSize)
        let ub = stepSize * Int(ceil(CGFloat(yMax)/CGFloat(stepSize)))
        // Build array
        var val = lb
        while true {
            result.append(val)
            val += stepSize
            if val > ub { break }
        }
        return result
    }

}
0
Petr Syrov

ES5 Javascriptでこれを必要とする人のために、少しレスリングしていますが、ここにあります:

var min=52;
var max=173;
var actualHeight=500; // 500 pixels high graph

var tickCount =Math.round(actualHeight/100); 
// we want lines about every 100 pixels.

if(tickCount <3) tickCount =3; 
var range=Math.abs(max-min);
var unroundedTickSize = range/(tickCount-1);
var x = Math.ceil(Math.log10(unroundedTickSize)-1);
var pow10x = Math.pow(10, x);
var roundedTickRange = Math.ceil(unroundedTickSize / pow10x) * pow10x;
var min_rounded=roundedTickRange * Math.floor(min/roundedTickRange);
var max_rounded= roundedTickRange * Math.ceil(max/roundedTickRange);
var nr=tickCount;
var str="";
for(var x=min_rounded;x<=max_rounded;x+=roundedTickRange)
{
    str+=x+", ";
}
console.log("Nice Y axis "+str);    

Toon Krijtjeの優れた回答に基づいています。

0
Hjalmar Snoep

上記のアルゴリズムでは、最小値と最大値の間の範囲が小さすぎる場合は考慮されません。そして、これらの値がゼロよりもはるかに高い場合はどうなりますか?次に、y軸をゼロより大きい値で開始する可能性があります。また、グラフの上側または下側にラインが完全に入らないようにするには、「呼吸する空気」を与えなければなりません。

これらのケースをカバーするために、私は(PHPで)上記のコードを書きました:

function calculateStartingPoint($min, $ticks, $times, $scale) {

    $starting_point = $min - floor((($ticks - $times) * $scale)/2);

    if ($starting_point < 0) {
        $starting_point = 0;
    } else {
        $starting_point = floor($starting_point / $scale) * $scale;
        $starting_point = ceil($starting_point / $scale) * $scale;
        $starting_point = round($starting_point / $scale) * $scale;
    }
    return $starting_point;
}

function calculateYaxis($min, $max, $ticks = 7)
{
    print "Min = " . $min . "\n";
    print "Max = " . $max . "\n";

    $range = $max - $min;
    $step = floor($range/$ticks);
    print "First step is " . $step . "\n";
    $available_steps = array(5, 10, 20, 25, 30, 40, 50, 100, 150, 200, 300, 400, 500);
    $distance = 1000;
    $scale = 0;

    foreach ($available_steps as $i) {
        if (($i - $step < $distance) && ($i - $step > 0)) {
            $distance = $i - $step;
            $scale = $i;
        }
    }

    print "Final scale step is " . $scale . "\n";

    $times = floor($range/$scale);
    print "range/scale = " . $times . "\n";

    print "floor(times/2) = " . floor($times/2) . "\n";

    $starting_point = calculateStartingPoint($min, $ticks, $times, $scale);

    if ($starting_point + ($ticks * $scale) < $max) {
        $ticks += 1;
    }

    print "starting_point = " . $starting_point . "\n";

    // result calculation
    $result = [];
    for ($x = 0; $x <= $ticks; $x++) {
        $result[] = $starting_point + ($x * $scale);
    }
    return $result;
}
0
panos

@Gamecatのアルゴリズムに基づいて、次のヘルパークラスを作成しました

public struct Interval
{
    public readonly double Min, Max, TickRange;

    public static Interval Find(double min, double max, int tickCount, double padding = 0.05)
    {
        double range = max - min;
        max += range*padding;
        min -= range*padding;

        var attempts = new List<Interval>();
        for (int i = tickCount; i > tickCount / 2; --i)
            attempts.Add(new Interval(min, max, i));

        return attempts.MinBy(a => a.Max - a.Min);
    }

    private Interval(double min, double max, int tickCount)
    {
        var candidates = (min <= 0 && max >= 0 && tickCount <= 8) ? new[] {2, 2.5, 3, 4, 5, 7.5, 10} : new[] {2, 2.5, 5, 10};

        double unroundedTickSize = (max - min) / (tickCount - 1);
        double x = Math.Ceiling(Math.Log10(unroundedTickSize) - 1);
        double pow10X = Math.Pow(10, x);
        TickRange = RoundUp(unroundedTickSize/pow10X, candidates) * pow10X;
        Min = TickRange * Math.Floor(min / TickRange);
        Max = TickRange * Math.Ceiling(max / TickRange);
    }

    // 1 < scaled <= 10
    private static double RoundUp(double scaled, IEnumerable<double> candidates)
    {
        return candidates.First(candidate => scaled <= candidate);
    }
}
0
Neil

質問と回答をありがとう、とても助かります。 Gamecat、ティック範囲を何に丸めるべきかをどのように決定しているのだろうかと思っています。

ティック範囲= 21.9。これは25.0でなければなりません

これをアルゴリズムで行うには、上記のアルゴリズムにロジックを追加して、より大きな数に対してこのスケールをうまく作成する必要がありますか?たとえば、10ティックの場合、範囲が3346の場合、ティック範囲は334.6に評価され、350のほうが適切な場合に最も近い10に丸めると340になります。

どう思いますか?

0
theringostarrs