web-dev-qa-db-fra.com

Choix d'une échelle linéaire attrayante pour l'axe Y d'un graphique

J'écris un peu de code pour afficher une barre graphique (ou une ligne) dans notre logiciel. Tout va bien. Ce qui me stoppe, c'est le marquage de l'axe des ordonnées.

L’appelant peut me dire à quel point il souhaite que l’échelle Y soit étiquetée, mais il me semble que je ne sais pas trop comment l’appeler de manière «attrayante». Je ne peux pas décrire "attrayant", et probablement vous non plus, mais nous le savons quand nous le voyons, non?

Donc, si les points de données sont:

   15, 234, 140, 65, 90

Et l’utilisateur demande 10 étiquettes sur l’axe des ordonnées. Un peu de papier et un crayon apparaissent:

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

Donc il y en a 10 (non compris 0), le dernier s'étend juste au-delà de la valeur la plus élevée (234 <250), et il s’agit d’un incrément "sympa" de 25 chacun. S'ils avaient demandé 8 étiquettes, une augmentation de 30 aurait semblé bien:

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

Neuf aurait été délicat. Peut-être juste avoir utilisé soit 8 ou 10 et l'appeler suffisamment proche serait bien. Et que faire lorsque certains points sont négatifs?

Je peux voir qu'Excel résout ce problème bien. 

Est-ce que quelqu'un connaît un algorithme à usage général (même une force brute est acceptable) pour résoudre ce problème? Je n'ai pas à le faire rapidement, mais ça devrait avoir l'air gentil.

66
Clinton Pierce

Il y a longtemps, j'ai écrit un module graphique qui couvrait bien cette question. Creuser dans la masse grise donne les résultats suivants:

  • Déterminez les limites inférieure et supérieure des données. (Attention au cas particulier où borne inférieure = borne supérieure!
  • Divisez la plage en la quantité requise de tiques.
  • Autour de la tique montent en belles quantités.
  • Ajustez les limites inférieure et supérieure en conséquence.

Prenons votre exemple:

15, 234, 140, 65, 90 with 10 ticks
  1. borne inférieure = 15
  2. borne supérieure = 234
  3. plage = 234-15 = 219
  4. plage de ticks = 21,9. Cela devrait être 25.0
  5. nouvelle limite inférieure = 25 * round (15/25) = 0
  6. nouvelle limite supérieure = 25 * round (1 + 235/25) = 250

Donc, la plage = 0,25,50, ..., 225,250

Vous pouvez obtenir la plage de ticks de Nice en procédant comme suit:

  1. diviser par 10 ^ x de telle sorte que le résultat se situe entre 0,1 et 1,0 (y compris 0,1 sauf 1).
  2. traduisez en conséquence:
    • 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. multiplier par 10 ^ x. 

Dans ce cas, 21.9 est divisé par 10 ^ 2 pour obtenir 0.219. Ceci est <= 0.25, nous avons donc 0.25. Multiplié par 10 ^ 2, cela donne 25.

Jetons un coup d'oeil au même exemple avec 8 ticks:

15, 234, 140, 65, 90 with 8 ticks
  1. borne inférieure = 15
  2. borne supérieure = 234
  3. plage = 234-15 = 219
  4. plage de ticks = 27,375
    1. Diviser par 10 ^ 2 pour 0.27375, se traduit par 0,3, ce qui donne (multiplié par 10 ^ 2) 30.
  5. nouvelle limite inférieure = 30 * round (15/30) = 0
  6. nouvelle limite supérieure = 30 * round (1 + 235/30) = 240

Lesquels donnent le résultat que vous avez demandé ;-).

------ Ajouté par KD ------

Voici le code qui réalise cet algorithme sans utiliser de tables de recherche, etc ...:

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;

En règle générale, le nombre de ticks inclut le tick inférieur, de sorte que les segments d'axe des ordonnées réels sont inférieurs de un au nombre de ticks.

90
Toon Krijthe

Voici un exemple PHP que j'utilise. Cette fonction renvoie un tableau de jolies valeurs sur l’axe Y, qui comprend les valeurs Y min et max qui ont été entrées. Bien entendu, cette routine peut également être utilisée pour les valeurs de l’axe X.

Cela vous permet de "suggérer" le nombre de ticks que vous voudrez peut-être, mais la routine retournera. J'ai ajouté des exemples de données et présenté les résultats.

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

Résultat obtenu à partir des données de l'échantillon

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

Essayez ce code. Je l'ai utilisé dans quelques scénarios de cartographie et cela fonctionne bien. C'est assez rapide aussi.

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;
    }
}
8
Drew Noakes

On dirait que l'appelant ne vous dit pas les gammes qu'il veut.

Vous êtes donc libre de modifier les points de terminaison jusqu'à ce que vous obteniez une bonne division par le nombre d'étiquettes.

Définissons "Nice". J'appellerais Nice si les étiquettes sont éteintes par:

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

Trouvez le max et le min de votre série de données. Appelons ces points:

min_point and max_point.

Maintenant, tout ce que vous avez à faire est de trouver 3 valeurs:

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

qui correspondent à l'équation:

(end_label - start_label)/label_offset == label_count

Il y a probablement beaucoup de solutions, alors choisissez-en une. La plupart du temps, je parie que vous pouvez définir 

start_label to 0

alors essayez simplement un entier différent

end_label

jusqu'à ce que l'offset soit "Nice"

6
Pyrolistical

Je me bats toujours avec ça :)

La réponse Gamecat originale semble fonctionner la plupart du temps, mais essayez de la connecter en indiquant "3 ticks" comme nombre de ticks requis (pour les mêmes valeurs de données 15, 234, 140, 65, 90) semble donner une plage de ticks de 73, ce qui après avoir divisé par 10 ^ 2 donne 0,73, ce qui correspond à 0,75, ce qui donne une plage de ticks "de Nice" de 75. 

Puis calculant la limite supérieure: 75 * round (1 + 234/75) = 300

et la limite inférieure: 75 * round (15/75) = 0

Mais évidemment, si vous commencez à 0 et que vous avancez par pas de 75 jusqu'à la limite supérieure de 300, vous vous retrouvez avec 0,75,150,225,300 .... ce qui est sans doute utile, ) pas les 3 ticks requis.

Juste frustrant que ça ne marche pas 100% du temps ... ce qui pourrait bien être dû à mon erreur quelque part bien sûr!

3
StillPondering

La réponse de Toon Krijthe fonctionne la plupart du temps. Mais parfois, cela produira un nombre excessif de tiques. Cela ne fonctionnera pas aussi avec les nombres négatifs. L’approche globale du problème est acceptable mais il existe un meilleur moyen de le gérer. L'algorithme que vous souhaitez utiliser dépendra de ce que vous voulez vraiment obtenir. Ci-dessous, je vous présente mon code que j'ai utilisé dans ma bibliothèque JS Ploting. Je l'ai testé et ça marche toujours (j'espère;)). Voici les principales étapes:

  • obtenez les extrémités globales xMin et xMax (incluez tous les tracés que vous voulez imprimer dans l'algorithme)
  • calculer la plage entre xMin et xMax 
  • calculer l'ordre de grandeur de votre portée
  • calculer la taille des ticks en divisant la plage par le nombre de ticks moins un
  • celui-ci est facultatif. Si vous souhaitez que le tick nul soit toujours imprimé, utilisez la taille de tick pour calculer le nombre de ticks positifs et négatifs. Le nombre total de ticks sera leur somme + 1 (le tick zéro)
  • celui-ci n'est pas nécessaire si vous avez toujours zéro tick imprimé. Calculer la limite inférieure et supérieure, mais n'oubliez pas de centrer l'intrigue

Commençons. D'abord les calculs de base

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

J'arrondis les valeurs minimale et maximale pour être sûr à 100% que mon graphique couvrira toutes les données. Il est également très important de garder le log10 au sol de la plage, qu’il soit négatif ou non, et de soustraire 1 plus tard. Sinon, votre algorithme ne fonctionnera pas pour les nombres inférieurs à un.

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

J'utilise "Nice looking ticks" pour éviter les ticks tels que 7, 13, 17, etc. La méthode que j'utilise ici est assez simple. Il est également agréable d'avoir zeroTick en cas de besoin. La parcelle a l'air beaucoup plus professionnelle de cette façon. Vous trouverez toutes les méthodes à la fin de cette réponse. 

Maintenant, vous devez calculer les limites supérieure et inférieure. C'est très facile avec zéro tick mais nécessite un peu plus d'effort dans les autres cas. Pourquoi? Parce que nous voulons bien centrer l'intrigue entre les limites supérieure et inférieure. Regardez mon code. Certaines des variables sont définies en dehors de cette portée et certaines d'entre elles sont des propriétés d'un objet dans lequel tout le code présenté est conservé. 

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

Et voici les méthodes que j'ai mentionnées avant que vous pouvez écrire vous-même mais vous pouvez également utiliser le mien

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

Il ne reste plus qu’une chose qui n’est pas incluse ici. C'est le "joli regard". Ce sont des bornes inférieures qui sont des nombres similaires aux numéros de "Nice ticks". Par exemple, il est préférable que la limite inférieure commence à 5 avec la taille de tick 5 plutôt que d'avoir un tracé qui commence à 6 avec la même taille de tick. Mais c'est mon viré, je vous le laisse.

J'espère que ça aide. À votre santé!

3
Arthur

cela fonctionne comme un charme, si vous voulez 10 étapes + zéro

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

Converti cette réponse as 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
    }

}
0
Petr Syrov

Pour ceux qui ont besoin de cela en Javascript ES5, ça fait un peu de la lutte, mais le voici:

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

Basé sur l'excellente réponse de Toon Krijtje.

0
Hjalmar Snoep

Les algorithmes ci-dessus ne prennent pas en compte le cas où la plage entre les valeurs min et max est trop petite. Et si ces valeurs sont beaucoup plus élevées que zéro? Ensuite, nous avons la possibilité de démarrer l’axe des y avec une valeur supérieure à zéro. De plus, afin d'éviter que notre ligne ne se trouve entièrement en haut ou en bas du graphique, nous devons lui donner un peu d'air pour respirer. 

Pour couvrir ces cas, j'ai écrit (sous PHP) le code ci-dessus:

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

Basé sur l'algorithme de @ Gamecat, j'ai produit la classe d'assistance suivante

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
Neil

Merci pour la question et la réponse, très utile. Gamecat, je me demande comment vous déterminez à quoi la plage de ticks doit être arrondie. 

plage de ticks = 21,9. Cela devrait être 25.0

Pour faire cela de manière algorithmique, il faudrait ajouter une logique à l'algorithme ci-dessus pour rendre cette échelle bien pour les grands nombres? Par exemple, avec 10 ticks, si la plage est 3346, la plage de ticks serait évaluée à 334,6 et l’arrondissement au 10 le plus proche donnerait 340 lorsque 350 est probablement plus agréable.

Qu'est-ce que tu penses?

0
theringostarrs