Auswahl einer attraktiven linearen Skala für die Y-Achse eines Diagramms

83

Ich schreibe ein bisschen Code, um ein Balkendiagramm (oder ein Liniendiagramm) in unserer Software anzuzeigen. Alles läuft gut. Das, was mich verblüfft hat, ist die Beschriftung der Y-Achse.

Der Anrufer kann mir sagen, wie fein er die Y-Skala beschriften möchte, aber ich scheine genau zu wissen, was er auf "attraktive" Weise beschriften soll. Ich kann "attraktiv" nicht beschreiben und Sie wahrscheinlich auch nicht, aber wir wissen es, wenn wir es sehen, oder?

Wenn also die Datenpunkte sind:

   15, 234, 140, 65, 90

Und der Benutzer fragt nach 10 Etiketten auf der Y-Achse. Ein bisschen Finagling mit Papier und Bleistift ergibt:

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

Es gibt also 10 (ohne 0), der letzte geht knapp über den höchsten Wert hinaus (234 <250) und es ist ein "schönes" Inkrement von jeweils 25. Wenn sie nach 8 Etiketten gefragt hätten, hätte ein Inkrement von 30 gut ausgesehen:

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

Neun wäre schwierig gewesen. Vielleicht habe ich nur 8 oder 10 verwendet und es nah genug genannt, wäre in Ordnung. Und was tun, wenn einige Punkte negativ sind?

Ich kann sehen, dass Excel dieses Problem gut angeht.

Kennt jemand einen Allzweckalgorithmus (selbst Brute Force ist in Ordnung), um dies zu lösen? Ich muss es nicht schnell machen, aber es sollte schön aussehen.

Clinton Pierce
quelle
1
Es gibt einige Informationen darüber, wie Excel die Max- und Min-Werte für seine Y-Achse auswählt: support.microsoft.com/kb/214075
Christopher Orr
Schöne Implementierung: stackoverflow.com/a/16363437/829571
Assylias

Antworten:

102

Vor langer Zeit habe ich ein Grafikmodul geschrieben, das dies gut abdeckt. Wenn Sie in die graue Masse graben, erhalten Sie Folgendes:

  • Bestimmen Sie die untere und obere Grenze der Daten. (Vorsicht vor dem Sonderfall, bei dem Untergrenze = Obergrenze!
  • Teilen Sie die Reichweite in die erforderliche Anzahl von Zecken.
  • Runden Sie den Zeckenbereich in schöne Mengen auf.
  • Passen Sie die Unter- und Obergrenze entsprechend an.

Nehmen wir Ihr Beispiel:

15, 234, 140, 65, 90 with 10 ticks
  1. Untergrenze = 15
  2. Obergrenze = 234
  3. Bereich = 234-15 = 219
  4. Tick-Bereich = 21,9. Dies sollte 25,0 sein
  5. neue Untergrenze = 25 * Runde (15/25) = 0
  6. neue Obergrenze = 25 * rund (1 + 235/25) = 250

Der Bereich ist also = 0,25,50, ..., 225,250

Sie können den schönen Tick-Bereich mit den folgenden Schritten erhalten:

  1. dividieren Sie durch 10 ^ x, so dass das Ergebnis zwischen 0,1 und 1,0 liegt (einschließlich 0,1 ohne 1).
  2. entsprechend übersetzen:
    • 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. multiplizieren Sie mit 10 ^ x.

In diesem Fall wird 21,9 durch 10 ^ 2 geteilt, um 0,219 zu erhalten. Dies ist <= 0,25, also haben wir jetzt 0,25. Multipliziert mit 10 ^ 2 ergibt dies 25.

Schauen wir uns das gleiche Beispiel mit 8 Ticks an:

15, 234, 140, 65, 90 with 8 ticks
  1. Untergrenze = 15
  2. Obergrenze = 234
  3. Bereich = 234-15 = 219
  4. Tick-Bereich = 27,375
    1. Teilen Sie durch 10 ^ 2 für 0,27375, was 0,3 bedeutet, was (multipliziert mit 10 ^ 2) 30 ergibt.
  5. neue Untergrenze = 30 * Runde (15/30) = 0
  6. neue Obergrenze = 30 * rund (1 + 235/30) = 240

Welche geben das gewünschte Ergebnis ;-).

------ Hinzugefügt von KD ------

Hier ist Code, der diesen Algorithmus ohne Verwendung von Nachschlagetabellen usw. erreicht:

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;

Im Allgemeinen enthält die Anzahl der Ticks das untere Tick, sodass die tatsächlichen Segmente der y-Achse eins weniger sind als die Anzahl der Ticks.

Toon Krijthe
quelle
1
Das war genau richtig. Schritt 3, ich musste X um 1 reduzieren. Um einen Bereich von 219 bis 0,1-> 1 zu erhalten, muss ich durch 10 ^ 3 (1000) und nicht durch 10 ^ 2 (100) dividieren. Ansonsten genau richtig.
Clinton Pierce
2
Sie beziehen sich auf das Teilen durch 10 ^ x und das Multiplizieren mit 10 ^ x. Es ist zu beachten, dass x folgendermaßen gefunden werden kann: 'double x = Math.Ceiling (Math.Log10 (tickRange));'
Bryan
1
Sehr hilfreich. Obwohl nicht verstanden - 'neue Untergrenze = 30 * Runde (15/30) = 0' (es wird 30 kommen, denke ich) und wie Sie 235 in 'neue Obergrenze = 30 * Runde (1 + 235/30) = bekommen haben 240 '235 wird nirgends erwähnt, es sollte 234 sein.
Mutant
4
Dies ist eine großartige Antwort. Sehr geschätzt.
Joel Anair
4
@ JoelAnair Danke, du hast gerade einen traurigen Tag ein bisschen heller gemacht.
Toon Krijthe
21

Hier ist ein PHP-Beispiel, das ich verwende. Diese Funktion gibt ein Array hübscher Y-Achsenwerte zurück, die die übergebenen minimalen und maximalen Y-Werte umfassen. Natürlich kann diese Routine auch für X-Achsenwerte verwendet werden.

Sie können "vorschlagen", wie viele Ticks Sie möchten, aber die Routine gibt zurück, was gut aussieht. Ich habe einige Beispieldaten hinzugefügt und die Ergebnisse für diese gezeigt.

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

Ergebnisausgabe aus Beispieldaten

# ./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
quelle
Mein Chef wird sich darüber freuen - auch von mir positiv gestimmt n DANKE !!
Stephen Hazel
Gute Antwort! Ich konvertiere es zu Swift 4 stackoverflow.com/a/55151115/2670547
Petr Syrov
9

Versuchen Sie diesen Code. Ich habe es in einigen Diagrammszenarien verwendet und es funktioniert gut. Es ist auch ziemlich schnell.

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
quelle
6

Klingt so, als würde der Anrufer Ihnen nicht die gewünschten Bereiche mitteilen.

Sie können also die Endpunkte so lange ändern, bis Sie sie durch Ihre Etikettenanzahl gut teilbar machen.

Definieren wir "schön". Ich würde nett anrufen, wenn die Etiketten von sind:

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

Finden Sie das Maximum und das Minimum Ihrer Datenreihen. Nennen wir diese Punkte:

min_point and max_point.

Jetzt müssen Sie nur noch 3 Werte finden:

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

das passt zur Gleichung:

(end_label - start_label)/label_offset == label_count

Es gibt wahrscheinlich viele Lösungen, wählen Sie einfach eine aus. Ich wette, Sie können die meiste Zeit einstellen

start_label to 0

Probieren Sie einfach eine andere Ganzzahl aus

end_label

bis der Offset "schön" ist

Pyrolistisch
quelle
3

Ich kämpfe immer noch damit :)

Die ursprüngliche Gamecat-Antwort scheint die meiste Zeit zu funktionieren, aber versuchen Sie, beispielsweise "3 Ticks" als Anzahl der erforderlichen Ticks einzugeben (für die gleichen Datenwerte 15, 234, 140, 65, 90) scheint einen Tick-Bereich von 73 zu ergeben, der nach Division durch 10 ^ 2 0,73 ergibt, was 0,75 entspricht, was einen 'schönen' Tick-Bereich von 75 ergibt.

Berechnen Sie dann die Obergrenze: 75 * rund (1 + 234/75) = 300

und die Untergrenze: 75 * rund (15/75) = 0

Aber klar, wenn Sie bei 0 beginnen und in Schritten von 75 bis zur Obergrenze von 300 fortfahren, erhalten Sie 0,75,150,225,300 .... was zweifellos nützlich ist, aber es sind 4 Ticks (ohne 0), nicht die 3 Ticks erforderlich.

Nur frustrierend, dass es nicht 100% der Zeit funktioniert ... was natürlich irgendwo auf meinen Fehler zurückzuführen sein könnte!

Immer noch nachdenklich
quelle
Ursprünglich dachte man, das Problem könnte etwas mit Bryans vorgeschlagener Methode zur Ableitung von x zu tun haben, aber dies ist natürlich vollkommen genau.
StillPondering
3

Die Antwort von Toon Krijthe funktioniert die meiste Zeit. Aber manchmal wird es eine übermäßige Anzahl von Zecken produzieren. Es wird auch nicht mit negativen Zahlen funktionieren. Die allgemeine Herangehensweise an das Problem ist in Ordnung, aber es gibt einen besseren Weg, um damit umzugehen. Der Algorithmus, den Sie verwenden möchten, hängt davon ab, was Sie wirklich erhalten möchten. Im Folgenden präsentiere ich Ihnen meinen Code, den ich in meiner JS Ploting-Bibliothek verwendet habe. Ich habe es getestet und es funktioniert immer (hoffentlich;)). Hier sind die wichtigsten Schritte:

  • Holen Sie sich globale Extreme xMin und xMax (geben Sie alle Diagramme ein, die Sie im Algorithmus drucken möchten).
  • Berechnen Sie den Bereich zwischen xMin und xMax
  • Berechnen Sie die Größenordnung Ihres Bereichs
  • Berechnen Sie die Zeckengröße, indem Sie den Bereich durch die Anzahl der Zecken minus eins teilen
  • Dieser ist optional. Wenn Sie immer null Ticks drucken möchten, verwenden Sie die Tickgröße, um die Anzahl der positiven und negativen Ticks zu berechnen. Die Gesamtzahl der Ticks ist ihre Summe + 1 (das Null-Tick).
  • Dieser wird nicht benötigt, wenn Sie immer null Häkchen gedruckt haben. Berechnen Sie die untere und obere Grenze, aber denken Sie daran, das Diagramm zu zentrieren

Lasst uns beginnen. Zuerst die Grundberechnungen

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

Ich runde Minimal- und Maximalwerte, um 100% sicher zu sein, dass mein Plot alle Daten abdeckt. Es ist auch sehr wichtig, den Boden log10 des Bereichs zu bestimmen, ob er negativ ist oder nicht, und 1 später abzuziehen. Andernfalls funktioniert Ihr Algorithmus nicht für Zahlen, die kleiner als eins sind.

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

Ich benutze "gut aussehende Zecken", um Zecken wie 7, 13, 17 usw. zu vermeiden. Die Methode, die ich hier verwende, ist ziemlich einfach. Es ist auch schön, bei Bedarf ZeroTick zu haben. Die Handlung sieht auf diese Weise viel professioneller aus. Sie finden alle Methoden am Ende dieser Antwort.

Jetzt müssen Sie die oberen und unteren Grenzen berechnen. Dies ist mit null Tick sehr einfach, erfordert aber in anderen Fällen etwas mehr Aufwand. Warum? Weil wir die Handlung innerhalb der oberen und unteren Grenze schön zentrieren wollen. Schauen Sie sich meinen Code an. Einige der Variablen werden außerhalb dieses Bereichs definiert, andere sind Eigenschaften eines Objekts, in dem der gesamte dargestellte Code gespeichert ist.

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

Und hier sind Methoden, die ich zuvor erwähnt habe, die Sie selbst schreiben können, aber Sie können auch meine verwenden

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

Es gibt nur noch eine Sache, die hier nicht enthalten ist. Dies ist die "gut aussehende Grenze". Dies sind Untergrenzen, die Zahlen sind, die den Zahlen in "gut aussehenden Zecken" ähnlich sind. Zum Beispiel ist es besser, wenn die Untergrenze bei 5 mit der Zeckengröße 5 beginnt, als wenn ein Diagramm bei 6 mit der gleichen Zeckengröße beginnt. Aber das habe ich dir überlassen.

Ich hoffe es hilft. Prost!

Arthur
quelle
1

Dies funktioniert wie ein Zauber, wenn Sie 10 Schritte + Null wollen

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

Konvertierte diese Antwort als 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
quelle
1

Für alle, die dies in ES5 Javascript brauchen, ein bisschen gerungen haben, aber hier ist es:

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

Basierend auf der hervorragenden Antwort von Toon Krijtje.

Hjalmar Snoep
quelle
1

Diese Lösung basiert auf einem Java-Beispiel, das ich gefunden habe.

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]

Hühner
quelle
0

Danke für die Frage und Antwort, sehr hilfreich. Gamecat, ich frage mich, wie Sie bestimmen, auf welchen Tick-Bereich gerundet werden soll.

Tick-Bereich = 21,9. Dies sollte 25,0 sein

Um dies algorithmisch zu tun, müsste man dem obigen Algorithmus Logik hinzufügen, um diese Skala für größere Zahlen gut zu machen? Wenn beispielsweise bei 10 Ticks der Bereich 3346 beträgt, ergibt sich ein Tick-Bereich von 334,6, und eine Rundung auf die nächsten 10 ergibt 340, wenn 350 wahrscheinlich besser ist.

Was denken Sie?

Theringostarrs
quelle
Im Beispiel von @ Gamecat ist 334,6 => 0,3346, was auf 0,4 gehen sollte. Der Tick-Bereich wäre also tatsächlich 400, was eine ziemlich schöne Zahl ist.
Bryan
0

Basierend auf dem Algorithmus von @ Gamecat habe ich die folgende Hilfsklasse erstellt

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
quelle
0

Die obigen Algorithmen berücksichtigen nicht den Fall, wenn der Bereich zwischen Min- und Max-Wert zu klein ist. Und was ist, wenn diese Werte viel höher als Null sind? Dann haben wir die Möglichkeit, die y-Achse mit einem Wert über Null zu starten. Um zu vermeiden, dass sich unsere Linie vollständig auf der Ober- oder Unterseite des Diagramms befindet, müssen wir ihr etwas "Luft zum Atmen" geben.

Um diese Fälle abzudecken, habe ich (auf PHP) den obigen Code geschrieben:

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