93 Stimmen

Auswählen einer attraktiven linearen Skala für die Y-Achse eines Diagramms

Ich schreibe ein bisschen Code, um ein Balken- (oder Linien-) Diagramm in unserer Software anzuzeigen. Alles läuft gut. Die Sache, bei der ich nicht weiterkomme, ist die Beschriftung der Y-Achse.

Der Anrufer kann mir sagen, wie fein er die Y-Skala beschriftet haben möchte, aber ich scheine nicht genau zu wissen, wie ich sie in einer "attraktiven" Art und Weise beschriften soll. Ich kann "attraktiv" nicht beschreiben, und Sie wahrscheinlich auch nicht, aber wir erkennen es, wenn wir es sehen, oder?

Wenn die Datenpunkte also sind:

   15, 234, 140, 65, 90

Und der Benutzer fragt nach 10 Beschriftungen auf der Y-Achse, was nach ein wenig Fummelei mit Papier und Bleistift herauskommt:

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

Es sind also 10 (0 nicht mitgerechnet), der letzte reicht gerade über den höchsten Wert hinaus (234 < 250), und es ist eine "nette" Steigerung von jeweils 25. Hätten sie um 8 Etiketten gebeten, hätte eine Schrittweite von 30 gut ausgesehen:

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

Neun wären schwierig gewesen. Vielleicht hätte es gereicht, entweder 8 oder 10 zu nehmen und es als nahe genug zu bezeichnen. Und was ist zu tun, wenn einige der Punkte negativ sind?

Ich kann sehen, dass Excel dieses Problem gut löst.

Kennt jemand einen allgemeinen Algorithmus (auch ein Brute-Force-Verfahren ist in Ordnung), um dieses Problem zu lösen? Ich muss es nicht schnell machen, aber es sollte gut aussehen.

3voto

Arthur Punkte 169

Die Antwort von Toon Krijthe funktioniert die meiste Zeit. Aber manchmal erzeugt es eine zu hohe Anzahl von Ticks. Es funktioniert auch nicht mit negativen Zahlen. Die allgemeine Herangehensweise an das Problem ist in Ordnung, aber es gibt einen besseren Weg, das Problem zu lösen. Welchen Algorithmus Sie verwenden wollen, hängt davon ab, was Sie wirklich erreichen wollen. Im Folgenden stelle ich Ihnen meinen Code vor, den ich in meiner JS Ploting-Bibliothek verwendet habe. Ich habe ihn getestet und er funktioniert immer (hoffentlich ;) ). Hier sind die wichtigsten Schritte:

  • Ermitteln der globalen Extremwerte xMin und xMax (alle zu druckenden Diagramme in den Algorithmus einbeziehen)
  • Bereich zwischen xMin und xMax berechnen
  • Berechnen Sie die Größenordnung Ihrer Reichweite
  • Berechnung der Tickgröße durch Division des Bereichs durch die Anzahl der Ticks minus eins
  • dies ist fakultativ. Wenn Sie möchten, dass immer ein Null-Tick gedruckt wird, verwenden Sie die Tick-Größe, um die Anzahl der positiven und negativen Ticks zu berechnen. Die Gesamtzahl der Ticks ist deren Summe + 1 (der Null-Tick)
  • Dies ist nicht erforderlich, wenn Sie immer Null-Zeichen drucken lassen. Berechnen Sie die untere und obere Grenze, aber denken Sie daran, die Darstellung zu zentrieren

Fangen wir an. Zunächst die grundlegenden Berechnungen

    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 die Minimal- und Maximalwerte, um 100 % sicher zu sein, dass mein Diagramm alle Daten abdeckt. Es ist auch sehr wichtig, den log10-Wert des Bereichs zu unterteilen, unabhängig davon, ob er negativ ist oder nicht, und später 1 zu subtrahieren. 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 verwende "schön aussehende Häkchen", um Häkchen wie 7, 13, 17 usw. zu vermeiden. Die Methode, die ich hier verwende, ist ziemlich einfach. Es ist auch schön, zeroTick haben, wenn nötig. Der Plot sieht auf diese Weise viel professioneller aus. Sie finden alle Methoden am Ende dieser Antwort.

Nun müssen Sie Ober- und Untergrenzen berechnen. Dies ist bei einem Nulltick sehr einfach, erfordert aber in anderen Fällen ein wenig mehr Aufwand. Warum? Weil wir das Diagramm innerhalb der oberen und unteren Grenze zentrieren wollen. Werfen Sie einen Blick auf meinen Code. Einige der Variablen sind außerhalb dieses Bereichs definiert und einige von ihnen sind Eigenschaften eines Objekts, in dem der gesamte vorgestellte Code enthalten 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 die Methoden, die ich bereits 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 aufgeführt ist. Das sind die "schön aussehenden Schranken". Dabei handelt es sich um untere Grenzen, die den Zahlen in den "schön aussehenden Ticks" ähnlich sind. Zum Beispiel ist es besser, wenn die untere Grenze bei 5 mit der Tickgröße 5 beginnt, als wenn die Grafik bei 6 mit der gleichen Tickgröße beginnt. Aber das überlasse ich Ihnen.

Ich hoffe, es hilft. Prost!

3voto

Petr Syrov Punkte 14121

Konvertiert diese réponse als スウィフト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
    }

}

2voto

chickens Punkte 14182

Diese Lösung basiert auf einer Java-Beispiel Ich fand.

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]

1voto

mario Punkte 11

Das 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

1voto

panos Punkte 29

Die obigen Algorithmen berücksichtigen nicht den Fall, dass die Spanne zwischen Min- und Max-Wert zu klein ist. Und was ist, wenn diese Werte viel größer als Null sind? Dann haben wir die Möglichkeit, die y-Achse mit einem Wert größer als Null zu beginnen. Um zu vermeiden, dass sich unsere Linie ganz am oberen oder unteren Rand des Diagramms befindet, müssen wir ihr etwas "Luft zum Atmen" geben.

Um diese Fälle abzudecken, habe ich (in 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;
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X