[algorithm] 그래프의 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이이 문제를 잘 해결하는 것을 볼 수 있습니다.

누구든지이 문제를 해결하기위한 범용 알고리즘 (무차별 대입도 괜찮음)을 알고 있습니까? 빨리 할 필요는 없지만 멋져 보일 것입니다.



답변

오래 전에 저는 이것을 잘 다루는 그래프 모듈을 작성했습니다. 회색 덩어리를 파는 것은 다음을 얻습니다.

  • 데이터의 하한과 상한을 결정합니다. (하한 = 상한 인 특수한 경우에주의하십시오!
  • 범위를 필요한 틱 수로 나눕니다.
  • 눈금 범위를 좋은 양으로 반올림하십시오.
  • 그에 따라 하한과 상한을 조정합니다.

예를 들어 보겠습니다.

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 축 세그먼트는 틱 수보다 하나 적습니다.


답변

다음은 내가 사용하는 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
)


답변

이 코드를 사용해보십시오. 몇 가지 차트 시나리오에서 사용했으며 잘 작동합니다. 너무 빠릅니다.

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


답변

발신자가 원하는 범위를 알려주지 않는 것 같습니다.

따라서 레이블 수로 잘 나눌 때까지 끝점을 자유롭게 변경할 수 있습니다.

“nice”를 정의합시다. 레이블이 다음과 같이 꺼져 있으면 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

아마도 많은 솔루션이있을 것이므로 하나만 선택하십시오. 대부분의 경우 설정할 수 있습니다.

start_label to 0

그래서 다른 정수를 시도하십시오

end_label

오프셋이 “nice”가 될 때까지


답변

나는 아직도 이것과 싸우고있다 🙂

원래 Gamecat 답변은 대부분의 경우 작동하는 것 같지만 필요한 틱 수 (동일한 데이터 값 15, 234, 140, 65, 90)로 “3 틱”이라고 연결해보십시오 …. it 틱 범위가 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 % 작동하지 않는다는 사실이 실망 스러웠습니다. 물론 어딘가에서 내 실수로 귀결 될 수 있습니다!


답변

Toon Krijthe 의 대답은 대부분의 경우 작동합니다. 그러나 때로는 과도한 수의 진드기를 생성합니다. 음수에서도 작동하지 않습니다. 문제에 대한 전반적인 접근 방식은 괜찮지 만이를 처리하는 더 좋은 방법이 있습니다. 사용하려는 알고리즘은 실제로 얻고 자하는 것에 따라 달라집니다. 아래에서는 JS Ploting 라이브러리에서 사용한 코드를 보여 드리겠습니다. 나는 그것을 테스트했으며 항상 작동합니다 (희망적으로;)). 주요 단계는 다음과 같습니다.

  • 전역 극한값 xMin 및 xMax 가져 오기 (알고리즘에서 인쇄하려는 모든 플롯을 포함하지 않음)
  • xMin과 xMax 사이의 범위 계산
  • 범위의 크기 차수 계산
  • 범위를 틱 수에서 1을 뺀 값으로 나누어 틱 크기 계산
  • 이것은 선택 사항입니다. 항상 0 눈금을 인쇄하려면 눈금 크기를 사용하여 양수 및 음수 눈금 수를 계산합니다. 총 틱 수는 합계 + 1 (제로 틱)입니다.
  • 항상 눈금이 0 인 경우에는 필요하지 않습니다. 하한과 상한을 계산하지만 플롯을 중앙에 배치해야합니다.

시작하자. 먼저 기본 계산

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

여기에 포함되지 않은 것이 하나 더 있습니다. 이것은 “멋져 보이는 경계”입니다. 이것은 “멋진 틱”의 숫자와 유사한 숫자 인 하한입니다. 예를 들어, 동일한 틱 크기로 6에서 시작하는 플롯을 갖는 것보다 틱 크기 5로 시작하는 하한을 5에서 시작하는 것이 좋습니다. 그러나 이것은 내 해고 나는 그것을 당신에게 맡깁니다.

도움이되기를 바랍니다. 건배!


답변

답변Swift 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
    }

}