Wybór atrakcyjnej skali liniowej dla osi Y wykresu

84

Piszę trochę kodu, aby wyświetlić wykres słupkowy (lub liniowy) w naszym oprogramowaniu. Wszystko w porządku. Zaskoczyło mnie etykietowanie osi Y.

Dzwoniący może mi powiedzieć, jak precyzyjnie chce, aby skala Y była oznaczona, ale wydaje mi się, że utknąłem na tym, co dokładnie nazwać ich w „atrakcyjny” sposób. Nie potrafię opisać „atrakcyjnego” i prawdopodobnie ty też nie możesz, ale wiemy to, kiedy to widzimy, prawda?

Więc jeśli punkty danych to:

   15, 234, 140, 65, 90

A użytkownik prosi o 10 etykiet na osi Y, trochę pracy z papierem i ołówkiem daje:

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

Więc jest tam 10 (bez 0), ostatnia rozciąga się nieco poza najwyższą wartość (234 <250) i jest to „miły” przyrost o 25 każdy. Gdyby poprosili o 8 etykiet, przyrost o 30 wyglądałby ładnie:

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

Dziewięć byłoby trudne. Może po prostu użyłem 8 lub 10 i nazwałbym to wystarczająco blisko, byłoby w porządku. A co zrobić, gdy niektóre punkty są ujemne?

Widzę, że Excel ładnie rozwiązuje ten problem.

Czy ktoś zna algorytm ogólnego przeznaczenia (nawet jakaś brutalna siła jest w porządku) do rozwiązania tego problemu? Nie muszę tego robić szybko, ale powinno ładnie wyglądać.

Clinton Pierce
źródło
1
Informacje o tym, jak program Excel wybiera wartości maksymalne i minimalne dla swojej osi Y, można znaleźć tutaj: support.microsoft.com/kb/214075
Christopher Orr
Niezła implementacja: stackoverflow.com/a/16363437/829571
assylias

Odpowiedzi:

103

Dawno temu napisałem moduł wykresu, który ładnie to opisał. Kopanie w szarej masie daje następujące efekty:

  • Określ dolną i górną granicę danych. (Uważaj na specjalny przypadek, w którym dolna granica = górna granica!
  • Podziel zakres na wymaganą liczbę ticków.
  • Zaokrąglij zakres tick w górę do niezłych kwot.
  • Dostosuj odpowiednio dolną i górną granicę.

Weźmy przykład:

15, 234, 140, 65, 90 with 10 ticks
  1. dolna granica = 15
  2. górna granica = 234
  3. zakres = 234-15 = 219
  4. zakres kleszczy = 21,9. Powinno to być 25,0
  5. nowa dolna granica = 25 * okrągła (15/25) = 0
  6. nowa górna granica = 25 * okrągła (1 + 235/25) = 250

Zatem zakres = 0,25,50, ..., 225,250

Możesz uzyskać ładny zakres ticków, wykonując następujące czynności:

  1. podziel przez 10 ^ x tak, aby wynik zawierał się między 0,1 a 1,0 (w tym 0,1 z wyłączeniem 1).
  2. przetłumacz odpowiednio:
    • 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. pomnóż przez 10 ^ x.

W tym przypadku 21,9 jest dzielone przez 10 ^ 2, aby uzyskać 0,219. To jest <= 0,25, więc mamy teraz 0,25. Po pomnożeniu przez 10 ^ 2 daje to 25.

Spójrzmy na ten sam przykład z 8 zaznaczeniami:

15, 234, 140, 65, 90 with 8 ticks
  1. dolna granica = 15
  2. górna granica = 234
  3. zakres = 234-15 = 219
  4. zakres kleszczy = 27,375
    1. Podziel przez 10 ^ 2 dla 0,27375, daje 0,3, co daje (pomnożone przez 10 ^ 2) 30.
  5. nowa dolna granica = 30 * okrążenie (15/30) = 0
  6. nowa górna granica = 30 * runda (1 + 235/30) = 240

Które dają wynik, o który prosiłeś ;-).

------ Dodane przez KD ------

Oto kod, który osiąga ten algorytm bez użycia tabel przeglądowych itp.:

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;

Ogólnie rzecz biorąc, liczba taktów obejmuje dolny tik, więc rzeczywiste segmenty osi Y są o jeden mniejsze niż liczba taktów.

Toon Krijthe
źródło
1
To było w porządku. Krok 3, musiałem zmniejszyć X o 1. Aby uzyskać zakres od 219 do .1-> 1, muszę podzielić przez 10 ^ 3 (1000), a nie 10 ^ 2 (100). W przeciwnym razie, na miejscu.
Clinton Pierce
2
Odwołujesz się do dzielenia przez 10 ^ x i mnożenia przez 10 ^ x. Należy zauważyć, że x można znaleźć w ten sposób: 'double x = Math.Ceiling (Math.Log10 (tickRange));'
Bryan,
1
Bardzo pomocny. Chociaż nie rozumiem - `` nowa dolna granica = 30 * runda (15/30) = 0 '' (myślę, że nadejdzie 30) i jak uzyskałeś 235 w 'nowej górnej granicy = 30 * runda (1 + 235/30) = 240 '235 nie jest nigdzie wymienione, powinno być 234.
Mutant
4
To świetna odpowiedź. Bardzo cenione.
Joel Anair,
4
@JoelAnair Dzięki, że ten smutny dzień stał się trochę jaśniejszy.
Toon Krijthe
22

Oto przykład PHP, którego używam. Ta funkcja zwraca tablicę ładnych wartości osi Y, które obejmują przekazane minimalne i maksymalne wartości Y. Oczywiście ta procedura może być również użyta dla wartości osi X.

Pozwala to „zasugerować”, ile tyknięć chcesz, ale procedura zwróci to, co wygląda dobrze. Dodałem kilka przykładowych danych i pokazałem ich wyniki.

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

Wynik wynikowy z przykładowych danych

# ./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
)
Scott Guthrie
źródło
mój szef będzie z tego zadowolony - ode mnie też zagłosuj n DZIĘKI !!
Stephen Hazel
Świetna odpowiedź! Przekształcam to na Swift 4 stackoverflow.com/a/55151115/2670547
Petr Syrov
@Scott Guthrie: To jest świetne, chyba że dane wejściowe nie są liczbami całkowitymi i są małymi liczbami, na przykład, jeśli yMin = 0,03 i yMax = 0,11.
Greg
9

Wypróbuj ten kod. Użyłem go w kilku scenariuszach wykresów i działa dobrze. Jest też dość szybki.

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;
    }
}
Drew Noakes
źródło
6

Wygląda na to, że dzwoniący nie podaje żądanych zakresów.

Możesz więc zmieniać punkty końcowe, dopóki nie uzyskasz ładnego podzielenia przez liczbę etykiet.

Zdefiniujmy „miły”. Powiedziałbym fajnie, gdyby etykiety były wyłączone przez:

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

Znajdź maksimum i minimum serii danych. Nazwijmy te punkty:

min_point and max_point.

Teraz wszystko, co musisz zrobić, to znaleźć 3 wartości:

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

które pasują do równania:

(end_label - start_label)/label_offset == label_count

Prawdopodobnie jest wiele rozwiązań, więc wybierz tylko jedno. W większości przypadków możesz ustawić

start_label to 0

więc po prostu wypróbuj inną liczbę całkowitą

end_label

dopóki przesunięcie nie będzie „ładne”

Pirolistyczne
źródło
3

Nadal z tym walczę :)

Oryginalna odpowiedź Gamecat wydaje się działać przez większość czasu, ale spróbuj podłączyć, powiedzmy, „3 tiki” jako liczbę wymaganych taktów (dla tych samych wartości danych 15, 234, 140, 65, 90) .... wydaje się dawać zakres tickowy równy 73, co po podzieleniu przez 10 ^ 2 daje 0,73, co odpowiada 0,75, co daje `` ładny '' zakres tikowania równy 75.

Następnie obliczając górną granicę: 75 * okrągłe (1 + 234/75) = 300

a dolna granica: 75 * okrągłe (15/75) = 0

Ale oczywiście jeśli zaczniesz od 0 i będziesz postępować w krokach od 75 do górnej granicy 300, otrzymasz 0,75,150,225,300 ... co jest bez wątpienia przydatne, ale są to 4 tiki (bez 0), a nie Wymagane 3 tiki.

Po prostu frustrujące, że to nie działa w 100% przypadków ... co oczywiście może wynikać z mojego błędu!

Wciąż się zastanawiam
źródło
Początkowo sądzono, że problem może mieć coś wspólnego z sugerowaną przez Bryana metodą wyprowadzania x, ale jest to oczywiście całkowicie dokładne.
StillPondering
3

Odpowiedź Toon Krijthe działa przez większość czasu. Ale czasami spowoduje to nadmierną liczbę kleszczy. Nie zadziała również z liczbami ujemnymi. Ogólne podejście do problemu jest w porządku, ale istnieje lepszy sposób rozwiązania tego problemu. Algorytm, którego chcesz użyć, będzie zależał od tego, co naprawdę chcesz uzyskać. Poniżej przedstawiam mój kod, którego użyłem w mojej bibliotece JS Ploting. Przetestowałem to i zawsze działa (miejmy nadzieję;)). Oto główne kroki:

  • pobierz globalne ekstremum xMin i xMax (w tym wszystkie wykresy, które chcesz wydrukować w algorytmie)
  • oblicz zakres od xMin do xMax
  • obliczyć rząd wielkości swojego zasięgu
  • obliczyć wielkość taktów, dzieląc zakres przez liczbę taktów minus jeden
  • ten jest opcjonalny. Jeśli chcesz, aby zawsze drukowano zero ticków, użyj rozmiaru tick do obliczenia liczby dodatnich i ujemnych ticków. Całkowita liczba ticków będzie ich sumą + 1 (zero tick)
  • ten nie jest potrzebny, jeśli zawsze drukowane jest zero znaczników. Oblicz dolną i górną granicę, ale pamiętaj, aby wycentrować działkę

Zaczynajmy. Najpierw podstawowe obliczenia

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

Zaokrąglam wartości minimalne i maksymalne, aby mieć 100% pewności, że mój wykres obejmie wszystkie dane. Bardzo ważne jest również, aby log10 podłogi zakresu, czy jest ujemny, czy nie, a następnie odjąć 1. W przeciwnym razie algorytm nie będzie działał dla liczb mniejszych niż jeden.

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

Używam „ładnie wyglądających kleszczy”, aby uniknąć kleszczy typu 7, 13, 17 itp. Metoda, której tu używam, jest dość prosta. Dobrze jest też mieć zeroTick w razie potrzeby. W ten sposób fabuła wygląda znacznie bardziej profesjonalnie. Znajdziesz wszystkie metody na końcu tej odpowiedzi.

Teraz musisz obliczyć górną i dolną granicę. Jest to bardzo łatwe przy zerowym ticku, ale w innym przypadku wymaga trochę więcej wysiłku. Czemu? Ponieważ chcemy ładnie wyśrodkować działkę w górnej i dolnej granicy. Spójrz na mój kod. Część zmiennych jest zdefiniowana poza tym zakresem, a część z nich jest właściwością obiektu, w którym przechowywany jest cały prezentowany kod.

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

A oto metody, o których wspomniałem wcześniej, a które możesz pisać samodzielnie, ale możesz też użyć moich

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

Jest jeszcze jedna rzecz, której nie ma tutaj. To jest „ładnie wyglądające granice”. Są to dolne granice, które są liczbami podobnymi do liczb w „ładnie wyglądających kleszczach”. Na przykład lepiej jest mieć dolną granicę zaczynającą się od 5 przy wielkości tiku 5, niż mieć wykres, który zaczyna się od 6 o tej samej wielkości tiku. Ale to mój ogień, zostawiam to tobie.

Mam nadzieję, że to pomoże. Twoje zdrowie!

Arthur
źródło
2

Przekonwertowano tę odpowiedź na 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
    }

}
Petr Syrov
źródło
Jest to świetne, chyba że dane wejściowe nie są liczbami całkowitymi i są małymi liczbami, na przykład, jeśli yMin = 0,03 i yMax = 0,11.
Greg
1

to działa jak urok, jeśli chcesz 10 kroków + zero

//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
mario
źródło
1

Dla każdego, kto potrzebuje tego w Javascript ES5, trochę się zmagał, ale oto jest:

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

Na podstawie doskonałej odpowiedzi Toon Krijtje.

Hjalmar Snoep
źródło
1

To rozwiązanie jest oparte na znalezionym przeze mnie przykładzie 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]

kurczaki
źródło
0

Dzięki za pytanie i odpowiedź, bardzo pomocne. Gamecat, zastanawiam się, w jaki sposób określasz, do jakiego zakresu tików należy zaokrąglić.

zakres kleszczy = 21,9. Powinno to być 25,0

Aby to zrobić algorytmicznie, należałoby dodać logikę do powyższego algorytmu, aby ładnie wykonać tę skalę dla większych liczb? Na przykład przy 10 tyknięciach, jeśli zakres wynosi 3346, wtedy zakres tików wyniesie 334,6, a zaokrąglenie do najbliższych 10 da 340, podczas gdy 350 jest prawdopodobnie lepsze.

Co myślisz?

theringostarrs
źródło
W przykładzie @ Gamecat 334,6 => 0,3346, co powinno oznaczać 0,4. Zatem przedział tikowy faktycznie wynosiłby 400, co jest całkiem niezłą liczbą.
Bryan,
0

Na podstawie algorytmu @ Gamecat stworzyłem następującą klasę pomocniczą

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);
    }
}
Neil
źródło
0

Powyższe algorytmy nie uwzględniają przypadku, gdy zakres między wartością minimalną a maksymalną jest zbyt mały. A co, jeśli te wartości są dużo większe niż zero? Następnie mamy możliwość rozpoczęcia osi y wartością większą od zera. Ponadto, aby uniknąć sytuacji, w której nasza linia znajduje się całkowicie w górnej lub dolnej części wykresu, musimy dać jej trochę „powietrza do oddychania”.

Aby opisać te przypadki, napisałem (w PHP) powyższy kod:

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;
}
panoramy
źródło