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ć.
Odpowiedzi:
Dawno temu napisałem moduł wykresu, który ładnie to opisał. Kopanie w szarej masie daje następujące efekty:
Weźmy przykład:
Zatem zakres = 0,25,50, ..., 225,250
Możesz uzyskać ładny zakres ticków, wykonując następujące czynności:
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:
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.:
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.
źródło
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
źródło
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; } }
źródło
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:
Znajdź maksimum i minimum serii danych. Nazwijmy te punkty:
Teraz wszystko, co musisz zrobić, to znaleźć 3 wartości:
które pasują do równania:
Prawdopodobnie jest wiele rozwiązań, więc wybierz tylko jedno. W większości przypadków możesz ustawić
więc po prostu wypróbuj inną liczbę całkowitą
dopóki przesunięcie nie będzie „ładne”
źródło
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!
źródło
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:
Zaczynajmy. Najpierw podstawowe obliczenia
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.
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.
A oto metody, o których wspomniałem wcześniej, a które możesz pisać samodzielnie, ale możesz też użyć moich
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!
źródło
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 } }
źródło
to działa jak urok, jeśli chcesz 10 kroków + zero
źródło
Dla każdego, kto potrzebuje tego w Javascript ES5, trochę się zmagał, ale oto jest:
Na podstawie doskonałej odpowiedzi Toon Krijtje.
źródło
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]
źródło
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ć.
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?
źródło
Na podstawie algorytmu @ Gamecat stworzyłem następującą klasę pomocniczą
źródło
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:
źródło