88 votes

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

J'écris un peu de code pour afficher un graphique à barres (ou linéaire) dans notre logiciel. Tout se passe bien. Ce qui m'embête, c'est l'étiquetage de l'axe Y. Je ne sais pas comment faire.

L'appelant peut me dire avec quelle finesse il veut que l'échelle Y soit étiquetée, mais je semble être bloqué sur la façon exacte de l'étiqueter d'une manière "attractive". Je ne peux pas décrire ce qui est "attirant", et probablement vous non plus, mais nous le savons quand nous le voyons, n'est-ce pas ?

Donc si les points de données sont :

   15, 234, 140, 65, 90

Et l'utilisateur demande 10 étiquettes sur l'axe des Y, un petit tour de passe-passe avec du papier et un crayon permet d'obtenir ce résultat :

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

Il y en a donc 10 (sans compter le 0), le dernier s'étend juste au-delà de la valeur la plus élevée (234 < 250), et c'est un "bel" incrément de 25 chacun. S'ils avaient demandé 8 étiquettes, un incrément de 30 aurait été agréable :

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

Neuf aurait été délicat. Il aurait suffi d'utiliser 8 ou 10 et de dire que c'était assez proche. Et que faire quand certains des points sont négatifs ?

Je vois qu'Excel s'attaque bien à ce problème.

Quelqu'un connaît-il un algorithme général (même une force brute est acceptable) pour résoudre ce problème ? Je n'ai pas besoin de le faire rapidement, mais le résultat doit être beau.

1 votes

Vous trouverez ici des informations sur la manière dont Excel choisit les valeurs maximales et minimales pour son axe Y : support.microsoft.com/kb/214075

0 votes

Belle mise en œuvre : stackoverflow.com/a/16363437/829571

107voto

Toon Krijthe Points 36327

Il y a longtemps, j'ai écrit un module graphique qui couvrait bien cette question. En creusant dans la masse grise, on obtient ce qui suit :

  • Déterminer la limite inférieure et supérieure des données. (Attention au cas particulier où la borne inférieure = borne supérieure !
  • Divisez la plage en nombre de ticks requis.
  • Arrondissez la fourchette de ticks en de 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. limite inférieure = 15
  2. limite supérieure = 234
  3. portée = 234-15 = 219
  4. tick range = 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 gamme = 0,25,50,...,225,250

Les étapes suivantes permettent d'obtenir une plage de tics agréable :

  1. diviser par 10^x de telle sorte que le résultat soit compris entre 0,1 et 1,0 (y compris 0,1 sauf 1).
  2. traduire 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. Ce qui est <= 0,25, donc nous avons maintenant 0,25. Multiplié par 10^2 cela donne 25.

Reprenons le même exemple avec 8 ticks :

15, 234, 140, 65, 90 with 8 ticks
  1. limite inférieure = 15
  2. limite supérieure = 234
  3. portée = 234-15 = 219
  4. plage de tics = 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

Ce qui donne le résultat que vous avez demandé ;-).

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

Voici un code qui réalise cet algorithme sans utiliser de tables de consultation, 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 réels de l'axe des y sont inférieurs d'une unité au nombre de ticks.

1 votes

C'était juste ce qu'il fallait. Étape 3, je devais réduire X par 1. Pour obtenir une plage de 219 à .1->1, je dois diviser par 10^3 (1000) et non 10^2 (100). Sinon, c'est parfait.

2 votes

Vous faites référence à la division par 10^x et à la multiplication par 10^x. Il convient de noter que x peut être trouvé de la manière suivante : 'double x = Math.Ceiling(Math.Log10(tickRange));'.

2 votes

Très utile. Mais je n'ai pas compris - 'new lower bound = 30 * round(15/30) = 0' (Il sera 30 je pense) et comment vous avez obtenu 235 dans 'new upper bound = 30 * round(1+235/30) = 240' 235 n'est mentionné nulle part, il devrait être 234.

22voto

Scott Guthrie Points 11

Voici un exemple de PHP que j'utilise. Cette fonction renvoie un tableau de jolies valeurs de l'axe des Y qui englobent les valeurs min et max de l'axe des Y transmises. Bien sûr, cette routine pourrait également être utilisée pour les valeurs de l'axe des X.

Il vous permet de "suggérer" le nombre de ticks que vous souhaitez, mais la routine retournera ce qui semble bon. J'ai ajouté quelques données d'exemple et montré les résultats pour celles-ci.

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

Sortie de résultats à partir de données échantillons

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

0 votes

Mon patron sera heureux avec ceci - upvote de moi aussi et MERCI ! !!

0 votes

Excellente réponse ! Je le convertis en Swift 4 stackoverflow.com/a/55151115/2670547

0 votes

@Scott Guthrie : C'est génial, sauf si les entrées ne sont pas des entiers et sont des petits nombres, par exemple, si yMin = 0,03 et yMax = 0,11.

9voto

Drew Noakes Points 69288

Essayez ce code. Je l'ai utilisé dans quelques scénarios graphiques et il fonctionne bien. Il est également assez rapide.

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

6voto

Pyrolistical Points 12457

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

Vous êtes donc libre de modifier les points d'arrivée jusqu'à ce que vous obteniez un résultat divisible par votre nombre d'étiquettes.

Définissons le terme "agréable". Je dirais que c'est bien si les étiquettes sont décalées de :

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 maximum et le minimum de vos séries de données. Appelons ces points :

min_point and max_point.

Maintenant, tout ce que vous devez 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 existe probablement de nombreuses solutions, alors choisissez-en une. La plupart du temps, je parie que vous pouvez régler

start_label to 0

donc essayez juste un autre entier

end_label

jusqu'à ce que le décalage soit "agréable"

3voto

StillPondering Points 31

Je suis toujours en train de me battre avec ça :)

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

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

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

Mais il est clair que si l'on commence à 0 et que l'on procède par pas de 75 jusqu'à la limite supérieure de 300, on obtient 0,75,150,225,300. ...., ce qui est sans doute utile, mais cela représente 4 ticks (sans compter le 0) et non les 3 ticks nécessaires.

C'est juste frustrant que cela ne fonctionne pas 100% du temps.... ce qui pourrait bien être dû à une erreur de ma part bien sûr !

0 votes

J'ai d'abord pensé que le problème pouvait être lié à la méthode de calcul de x suggérée par Bryan, mais celle-ci est bien sûr parfaitement exacte.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X