Bir grafiğin Y Ekseni için çekici bir doğrusal ölçek seçme


84

Yazılımımızda bir çubuk (veya çizgi) grafiği görüntülemek için biraz kod yazıyorum. Herşey yolunda gidiyor. Beni şaşırtan şey, Y eksenini etiketlemek.

Arayan kişi bana Y ölçeğinin ne kadar iyi etiketlenmesini istediklerini söyleyebilir, ama ben onları tam olarak neyi "çekici" bir şekilde etiketleyeceğime takılı kaldım. Ben "çekici" yi tanımlayamam ve muhtemelen sen de yapamazsın, ama onu gördüğümüzde biliyoruz, değil mi?

Yani veri noktaları ise:

   15, 234, 140, 65, 90

Ve kullanıcı Y ekseninde 10 etiket istiyor, biraz kağıt ve kalemle bitirme ortaya çıkıyor:

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

Yani orada 10 var (0 hariç), sonuncusu en yüksek değerin (234 <250) hemen ötesine uzanıyor ve her biri 25'lik "hoş" bir artış. 8 etiket isteselerdi, 30'luk bir artış güzel görünürdü:

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

Dokuz zor olurdu. Belki sadece 8 veya 10'u kullandınız ve yeterince yakın olarak adlandırın tamam olabilir. Ve bazı noktalar olumsuz olduğunda ne yapmalı?

Excel'in bu sorunu güzelce çözdüğünü görebiliyorum.

Bunu çözmek için genel amaçlı bir algoritma bilen var mı (biraz kaba kuvvet bile olabilir)? Hızlı yapmak zorunda değilim ama güzel görünmeli.


1
Excel'in Y ekseni için maksimum ve minimum değerleri nasıl seçtiği hakkında bazı bilgiler burada: support.microsoft.com/kb/214075
Christopher Orr

Yanıtlar:


103

Uzun zaman önce bunu güzelce anlatan bir grafik modülü yazmıştım. Gri kütleyi kazmak aşağıdakileri alır:

  • Verinin alt ve üst sınırını belirleyin. (Alt sınır = üst sınır olduğu özel duruma dikkat edin!
  • Aralığı gerekli sayıda keneye bölün.
  • Kene aralığını güzel miktarlara yuvarlayın.
  • Alt ve üst sınırı buna göre ayarlayın.

Örneğinizi ele alalım:

15, 234, 140, 65, 90 with 10 ticks
  1. alt sınır = 15
  2. üst sınır = 234
  3. aralık = 234-15 = 219
  4. kene aralığı = 21.9. Bu 25.0 olmalıdır
  5. yeni alt sınır = 25 * yuvarlak (15/25) = 0
  6. yeni üst sınır = 25 * yuvarlak (1 + 235/25) = 250

Yani aralık = 0,25,50, ..., 225,250

Güzel kene aralığını aşağıdaki adımlarla elde edebilirsiniz:

  1. sonuç 0.1 ile 1.0 arasında olacak şekilde (1 hariç 0.1 dahil) 10 ^ x'e bölün.
  2. uygun şekilde çevirin:
    • 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 ile çarpın.

Bu durumda, 21,9, 0,219 elde etmek için 10 ^ 2'ye bölünür. Bu <= 0.25, yani şimdi 0.25'imiz var. 10 ^ 2 ile çarpıldığında bu 25 verir.

Aynı örneğe 8 tik ile bir göz atalım:

15, 234, 140, 65, 90 with 8 ticks
  1. alt sınır = 15
  2. üst sınır = 234
  3. aralık = 234-15 = 219
  4. tik aralığı = 27.375
    1. 0.27375 için 10 ^ 2'ye bölün, 0.3'e çevirir, bu da (10 ^ 2 ile çarpılır) 30 verir.
  5. yeni alt sınır = 30 * yuvarlak (15/30) = 0
  6. yeni üst sınır = 30 * yuvarlak (1 + 235/30) = 240

İstediğiniz sonucu veren ;-).

------ KD tarafından eklendi ------

İşte bu algoritmayı arama tabloları vb. Kullanmadan gerçekleştiren kod:

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;

Genel olarak, işaretlerin sayısı alt onay işaretini içerir, bu nedenle gerçek y ekseni bölümleri, onay sayısından bir eksiktir.


1
Bu hemen hemen doğruydu. Adım 3, X'i 1 azaltmak zorunda kaldım. 219'dan .1-> 1'e bir aralık elde etmek için 10 ^ 2 (100) yerine 10 ^ 3 (1000) ile bölmem gerekiyor. Aksi takdirde, dikkat edin.
Clinton Pierce

2
10 ^ x'e bölmeyi ve 10 ^ x ile çarpmayı referans veriyorsunuz. X'in şu şekilde bulunabileceğine dikkat edilmelidir: 'double x = Math.Ceiling (Math.Log10 (tickRange));'
Bryan

1
Çok yararlı. Anlamamış olsam da - 'yeni alt sınır = 30 * tur (15/30) = 0' (Sanırım 30 gelecek) ve 'yeni üst sınır = 30 * turda (1 + 235/30) 235'i nasıl elde ettiğinizi 240 '235 hiçbir yerde bahsedilmiyor, 234 olmalı.
Mutant

4
Bu harika bir cevap. Çok müteşekkirim.
Joel Anair

4
@JoelAnair Teşekkürler, üzücü bir günü biraz daha parlak hale getirdin.
Toon Krijthe

22

İşte kullandığım bir PHP örneği. Bu fonksiyon, aktarılan minimum ve maksimum Y değerlerini kapsayan güzel Y ekseni değerleri dizisi döndürür. Elbette, bu rutin X ekseni değerleri için de kullanılabilir.

Kaç tik isteyebileceğinizi "önermenize" olanak tanır, ancak rutin, iyi görünen şeyi döndürür. Bazı örnek veriler ekledim ve bunların sonuçlarını gösterdim.

#!/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);
?>

Örnek verilerden sonuç çıktısı

# ./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
)

patronum bundan memnun olacak - benden de olumlu oy n TEŞEKKÜRLER !!
Stephen Hazel

Mükemmel cevap! Onu Swift 4 stackoverflow.com/a/55151115/2670547'ye
Petr Syrov

@Scott Guthrie: Girişler tam sayı olmadıkça ve küçük sayılar olmadıkça, örneğin yMin = 0.03 ve yMax = 0.11 ise bu harikadır.
Greg

9

Bu kodu deneyin. Birkaç grafik senaryosunda kullandım ve iyi çalışıyor. Aynı zamanda oldukça hızlı.

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

6

Görünüşe göre arayan kişi size istediği aralıkları söylemiyor.

Yani etiket sayınıza göre güzel bir şekilde bölünebilene kadar uç noktaları değiştirmekte özgürsünüz.

"Güzel" i tanımlayalım. Etiketleri bozarsa güzel derim:

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, ...

Veri serinizin maksimum ve minimum değerlerini bulun. Bu noktalara diyelim:

min_point and max_point.

Şimdi yapmanız gereken tek şey bulmak 3 değer:

- 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"

denkleme uyan:

(end_label - start_label)/label_offset == label_count

Muhtemelen birçok çözüm vardır, bu yüzden birini seçin. Bahse girerim çoğu zaman ayarlayabilirsin

start_label to 0

bu yüzden sadece farklı bir tam sayı deneyin

end_label

ofset "güzel" olana kadar


3

Hala bununla mücadele ediyorum :)

Orijinal Gamecat cevabı çoğu zaman işe yarıyor gibi görünüyor, ancak gerekli tik sayısı olarak "3 tik" takmayı deneyin (aynı veri değerleri için 15, 234, 140, 65, 90) .... o 73'lük bir tik aralığı veriyor gibi görünüyor, bu da 10 ^ 2'ye böldükten sonra 0,73 verir, bu da 0,75'le eşleşir, bu da 75'lik 'güzel' bir tik aralığı verir.

Sonra üst sınırı hesaplama: 75 * yuvarlak (1 + 234/75) = 300

ve alt sınır: 75 * yuvarlak (15/75) = 0

Ancak 0'dan başlarsanız ve 300'ün üst sınırına kadar 75'lik adımlarla ilerlerseniz, 0,75,150,225,300 .... ile sonuçlanırsınız. 3 işaret gerekli.

Sadece zamanın% 100'ünde çalışmaması sinir bozucu ... ki bu tabii ki bir yerde benim hatam olabilir!


Başlangıçta sorunun Bryan'ın önerdiği x türetme yöntemiyle bir ilgisi olabileceğini düşünmüştüm, ancak bu elbette mükemmel bir şekilde doğrudur.
StillPondering

3

Toon Krijthe'nin cevabı çoğu zaman işe yarıyor . Ancak bazen fazla sayıda kene üretecektir. Negatif sayılarla da çalışmaz. Soruna genel yaklaşım tamam ama bunu halletmenin daha iyi bir yolu var. Kullanmak istediğiniz algoritma, gerçekte ne elde etmek istediğinize bağlı olacaktır. Aşağıda JS Ploting kütüphanemde kullandığım kodu size sunuyorum. Test ettim ve her zaman işe yarıyor (umarım;)). İşte ana adımlar:

  • global extremas xMin ve xMax elde edin (algoritmada yazdırmak istediğiniz tüm grafikleri dahil edin)
  • xMin ve xMax arasındaki aralığı hesapla
  • Aralığınızın büyüklük sırasını hesaplayın
  • aralığı tik sayısı eksi bire bölerek tik boyutunu hesaplayın
  • bu isteğe bağlıdır. Her zaman sıfır onay işareti yazdırmak istiyorsanız, pozitif ve negatif tiklerin sayısını hesaplamak için tik boyutunu kullanırsınız. Toplam tik sayısı, toplamı +1 olacaktır (sıfır tik)
  • her zaman yazdırılmış sıfır onay işaretiniz varsa buna gerek yoktur. Alt ve üst sınırları hesaplayın, ancak grafiği ortalamayı unutmayın

Hadi başlayalım. İlk önce temel hesaplamalar

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

Grafikimin tüm verileri kapsayacağından% 100 emin olmak için minimum ve maksimum değerleri yuvarlıyorum. Negatif olsun ya da olmasın ve daha sonra 1'i çıkararak, aralığın kat log10'u için de çok önemlidir. Aksi takdirde, algoritmanız birden küçük sayılar için çalışmayacaktır.

    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 vb. Kenelerden kaçınmak için "hoş görünümlü keneler" kullanıyorum. Burada kullandığım yöntem oldukça basit. Gerektiğinde zeroTick'e sahip olmak da güzel. Konu bu şekilde çok daha profesyonel görünüyor. Tüm yöntemleri bu cevabın sonunda bulacaksınız.

Şimdi üst ve alt sınırları hesaplamanız gerekiyor. Sıfır tıklama ile bu çok kolaydır, ancak diğer durumda biraz daha fazla çaba gerektirir. Neden? Çünkü arsayı üst ve alt sınırların içinde güzelce ortalamak istiyoruz. Koduma bir bak. Değişkenlerden bazıları bu kapsamın dışında tanımlanmıştır ve bazıları da sunulan kodun tamamının tutulduğu bir nesnenin özellikleridir.

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

Ve işte daha önce bahsettiğim, kendiniz yazabileceğiniz ama benimkini de kullanabileceğiniz yöntemler

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

Burada yer almayan tek bir şey daha var. Bu "güzel görünen sınırlar" dır. Bunlar, "güzel görünümlü işaretler" deki sayılara benzer sayılar olan alt sınırlardır. Örneğin, aynı tik boyutu ile 6'da başlayan bir grafiğe sahip olmaktansa, 5 tik boyutu 5 ile başlayan alt sınıra sahip olmak daha iyidir. Ama bu benim kovulmamı sana bırakıyorum.

Umarım yardımcı olur. Şerefe!


2

Bu yanıtı Swift 4 olarak dönüştürdü

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
    }

}

Bu, girişler tam sayı olmadıkça ve küçük sayılar olmadığı sürece harikadır, örneğin, yMin = 0.03 ve yMax = 0.11 ise.
Greg

1

10 adım + sıfır istiyorsanız bu bir cazibe gibi çalışır

//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

ES5 Javascript'te buna ihtiyaç duyan herkes için biraz güreşiyor, ama işte burada:

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'nin mükemmel cevabına dayanmaktadır.


1

Bu çözüm, bulduğum bir Java örneğine dayanıyor .

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]


0

Soru ve cevap için teşekkürler, çok yardımcı oldu. Gamecat, tik aralığının neye yuvarlanması gerektiğini nasıl belirlediğini merak ediyorum.

kene aralığı = 21.9. Bu 25.0 olmalıdır

Bunu algoritmik olarak yapmak için, bu ölçeği daha büyük sayılar için güzel bir şekilde yapmak için yukarıdaki algoritmaya mantık eklemek gerekir mi? Örneğin, 10 tik ile, aralık 3346 ise, o zaman tik aralığı 334.6 olarak değerlendirilir ve en yakın 10'a yuvarlama, 350 muhtemelen daha güzel olduğunda 340 verir.

Ne düşünüyorsun?


@ Gamecat'ın örneğinde, 334.6 => 0.3346, bu 0.4'e gitmelidir. Yani tik aralığı, aslında oldukça güzel bir sayı olan 400 olacaktır.
Bryan

0

@ Gamecat algoritmasına dayanarak, aşağıdaki yardımcı sınıfı ürettim

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

Yukarıdaki algoritmalar, minimum ve maksimum değer arasındaki aralığın çok küçük olduğu durumu dikkate almaz. Ya bu değerler sıfırdan çok daha yüksekse? Ardından, y eksenini sıfırdan büyük bir değerle başlatma olanağımız var. Ayrıca, çizgimizin tamamen grafiğin üstünde veya altında olmasını önlemek için ona biraz "nefes alması için hava" vermeliyiz.

Yazdığım bu durumları (PHP'de) kapsamak için yukarıdaki kodu:

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;
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.