88

ソフトウェアで棒グラフ (または折れ線グラフ) を表示するコードを少し書いています。すべて順調です。私が困惑したのは、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 がこの問題にうまく取り組んでいることがわかります。

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

4

13 に答える 13

107

ずっと前に、これをうまくカバーするグラフモジュールを書きました。灰色の塊を掘ると、次のようになります。

  • データの下限と上限を決定します。(下限 = 上限という特殊なケースに注意してください!
  • 範囲を必要なティック数に分割します。
  • ティック範囲を適切な量に丸めます。
  • それに応じて下限と上限を調整します。

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

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 の間にあるように 10^x で除算します (1 を除く 0.1 を含む)。
  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 になり、(10^2 を掛けると) 30 になります。
  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 つ少なくなります。

于 2008-11-28T21:44:39.220 に答える
22

これが私が使用している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
)
于 2012-01-25T17:49:26.763 に答える
9

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

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;
    }
}
于 2009-10-06T12:07:51.030 に答える
6

発信者が必要な範囲を教えてくれないようです。

したがって、ラベル数でうまく割り切れるまで、エンドポイントを自由に変更できます。

「素敵」を定義しましょう。ラベルがオフになっている場合は、次のようにいいと思います。

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

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

于 2008-11-28T21:33:08.540 に答える
3

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

元の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から始めて、300の上限まで75のステップで進むと、0,75,150,225,300になります。これは間違いなく便利ですが、4ティック(0を含まない)であり、 3ティックが必要です。

それが100%の時間で機能しないことにイライラするだけです....もちろんどこかで私の間違いに帰着する可能性があります!

于 2012-11-10T11:43:37.677 に答える
1

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
于 2012-07-01T16:59:10.693 に答える
1

このソリューションは、私が見つけたJava の例に基づいています。

const niceScale = ( minPoint, maxPoint, maxTicks) => {
    const niceNum = ( localRange,  round) => {
        var exponent,fraction,niceFraction;
        exponent = Math.floor(Math.log10(localRange));
        fraction = localRange / Math.pow(10, exponent);
        if (round) {
            if (fraction < 1.5) niceFraction = 1;
            else if (fraction < 3) niceFraction = 2;
            else if (fraction < 7) niceFraction = 5;
            else niceFraction = 10;
        } else {
            if (fraction <= 1) niceFraction = 1;
            else if (fraction <= 2) niceFraction = 2;
            else if (fraction <= 5) niceFraction = 5;
            else niceFraction = 10;
        }
        return niceFraction * Math.pow(10, exponent);
    }
    const result = [];
    const range = niceNum(maxPoint - minPoint, false);
    const stepSize = niceNum(range / (maxTicks - 1), true);
    const lBound = Math.floor(minPoint / stepSize) * stepSize;
    const uBound = Math.ceil(maxPoint / stepSize) * stepSize;
    for(let i=lBound;i<=uBound;i+=stepSize) result.push(i);
    return result;
};
console.log(niceScale(15,234,6));
// > [0, 100, 200, 300]

于 2019-10-28T15:22:13.820 に答える
0

上記のアルゴリズムは、最小値と最大値の間の範囲が小さすぎる場合を考慮していません。そして、これらの値がゼロよりもはるかに高い場合はどうなるでしょうか? 次に、ゼロより大きい値で 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;
}
于 2016-09-30T12:08:08.357 に答える
0

@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);
    }
}
于 2012-12-21T11:26:01.103 に答える