web-dev-qa-db-fra.com

Algorithme pour "Nice" intervalles de ligne de grille sur un graphique

J'ai besoin d'un algorithme assez intelligent pour arriver à de "jolies" lignes de grille pour un graphique (graphique).

Par exemple, supposons un graphique à barres avec les valeurs 10, 30, 72 et 60. Vous savez:

Valeur minimale: 10 Valeur maximale: 72 Plage: 62

La première question est: de quoi partez-vous? Dans ce cas, 0 serait la valeur intuitive, mais cela ne tiendra pas sur d'autres ensembles de données, alors je suppose:

La valeur minimale de la grille doit être 0 ou une valeur "correcte" inférieure à la valeur minimale des données dans la plage. Alternativement, cela peut être spécifié.

La valeur maximale de la grille doit être une valeur "Nice" supérieure à la valeur maximale de la plage. Vous pouvez également le spécifier (par exemple, vous pouvez choisir entre 0 et 100 si vous affichez des pourcentages, quelles que soient les valeurs réelles).

Le nombre de lignes de grille (graduations) dans la plage doit être spécifié ou défini dans une plage donnée (par exemple, 3-8) de sorte que les valeurs soient "Nice" (c.-à-d. Des nombres ronds) et que vous maximisiez l'utilisation de la zone de graphique. Dans notre exemple, 80 serait un maximum raisonnable, car cela utiliserait 90% de la hauteur de la carte (72/80), alors que 100 créerait plus d'espace perdu.

Quelqu'un sait-il d'un bon algorithme pour cela? Le langage est hors de propos puisque je le mettrai en œuvre dans les conditions qui me sont propres.

54
cletus

CPAN fournit une implémentation ici (voir lien source)

Voir aussi Algorithme Tickmark pour un axe de graphe

FYI, avec vos données d'échantillon:

  • Maple: Min = 8, Max = 74, étiquettes = 10,20, .., 60,70, tiques = 10,12,14, .. 70,72
  • MATLAB: Min = 10, Max = 80, étiquettes = 10,20 , .., 60,80
9
Christoph Rüegg

Je l'ai fait avec une sorte de méthode de force brute. Tout d’abord, déterminez le nombre maximal de graduations que vous pouvez insérer dans l’espace. Diviser la plage totale de valeurs par le nombre de ticks; c'est le le minimum espacement de la tique. Calculez maintenant le plancher de la base 10 du logarithme pour obtenir l’ampleur du tick et divisez par cette valeur. Vous devriez vous retrouver avec quelque chose dans la plage de 1 à 10. Choisissez simplement le nombre de tours supérieur ou égal à la valeur et multipliez-le par le logarithme calculé précédemment. Ceci est votre dernier espacement des ticks.

Exemple en Python:

import math

def BestTick(largest, mostticks):
    minimum = largest / mostticks
    magnitude = 10 ** math.floor(math.log(minimum, 10))
    residual = minimum / magnitude
    if residual > 5:
        tick = 10 * magnitude
    Elif residual > 2:
        tick = 5 * magnitude
    Elif residual > 1:
        tick = 2 * magnitude
    else:
        tick = magnitude
    return tick

Edit: vous êtes libre de modifier la sélection des intervalles "de Nice". Un intervenant semble être insatisfait des sélections fournies, car le nombre réel de ticks peut être jusqu'à 2,5 fois inférieur au maximum. Voici une légère modification qui définit une table pour les intervalles de Nice. Dans l'exemple, j'ai développé les sélections pour que le nombre de ticks ne soit pas inférieur à 3/5 du maximum.

import bisect

def BestTick2(largest, mostticks):
    minimum = largest / mostticks
    magnitude = 10 ** math.floor(math.log(minimum, 10))
    residual = minimum / magnitude
    # this table must begin with 1 and end with 10
    table = [1, 1.5, 2, 3, 5, 7, 10]
    tick = table[bisect.bisect_right(table, residual)] if residual < 10 else 10
    return tick * magnitude
31
Mark Ransom

Il y a 2 pièces au problème:

  1. Déterminer l'ordre de grandeur impliqué, et
  2. Arrondis à quelque chose de pratique.

Vous pouvez gérer la première partie en utilisant des logarithmes:

range = max - min;  
exponent = int(log(range));       // See comment below.
magnitude = pow(10, exponent);

Ainsi, par exemple, si votre plage est comprise entre 50 et 1 200, l’exposant est égal à 3 et la magnitude à 1 000.

Ensuite, traitez la deuxième partie en décidant combien de subdivisions vous souhaitez dans votre grille:

value_per_division = magnitude / subdivisions;

Ceci est un calcul approximatif car l'exposant a été tronqué en un entier. Vous pouvez modifier le calcul de l’exposant pour mieux gérer les conditions aux limites, p. Ex. en arrondissant au lieu de prendre int() si vous vous retrouvez avec trop de subdivisions.

27
Adam Liss

J'utilise l'algorithme suivant. C'est semblable aux autres postés ici, mais c'est le premier exemple en C #.

public static class AxisUtil
{
    public static float CalcStepSize(float range, float targetSteps)
    {
        // calculate an initial guess at step size
        var tempStep = range/targetSteps;

        // get the magnitude of the step size
        var mag = (float)Math.Floor(Math.Log10(tempStep));
        var magPow = (float)Math.Pow(10, mag);

        // calculate most significant digit of the new step size
        var magMsd = (int)(tempStep/magPow + 0.5);

        // promote the MSD to either 1, 2, or 5
        if (magMsd > 5)
            magMsd = 10;
        else if (magMsd > 2)
            magMsd = 5;
        else if (magMsd > 1)
            magMsd = 2;

        return magMsd*magPow;
    }
}
13
Drew Noakes

Voici une autre implémentation en JavaScript:

var ln10 = Math.log(10);
var calcStepSize = function(range, targetSteps)
{
  // calculate an initial guess at step size
  var tempStep = range / targetSteps;

  // get the magnitude of the step size
  var mag = Math.floor(Math.log(tempStep) / ln10);
  var magPow = Math.pow(10, mag);

  // calculate most significant digit of the new step size
  var magMsd = Math.round(tempStep / magPow + 0.5);

  // promote the MSD to either 1, 2, or 5
  if (magMsd > 5.0)
    magMsd = 10.0;
  else if (magMsd > 2.0)
    magMsd = 5.0;
  else if (magMsd > 1.0)
    magMsd = 2.0;

  return magMsd * magPow;
};
5
Drew Noakes

Tiré de Mark ci-dessus, une classe Util légèrement plus complète en c #. Cela calcule également un premier et un dernier tick appropriés.

public  class AxisAssists
{
    public double Tick { get; private set; }

    public AxisAssists(double aTick)
    {
        Tick = aTick;
    }
    public AxisAssists(double range, int mostticks)
    {
        var minimum = range / mostticks;
        var magnitude = Math.Pow(10.0, (Math.Floor(Math.Log(minimum) / Math.Log(10))));
        var residual = minimum / magnitude;
        if (residual > 5)
        {
            Tick = 10 * magnitude;
        }
        else if (residual > 2)
        {
            Tick = 5 * magnitude;
        }
        else if (residual > 1)
        {
            Tick = 2 * magnitude;
        }
        else
        {
            Tick = magnitude;
        }
    }

    public double GetClosestTickBelow(double v)
    {
        return Tick* Math.Floor(v / Tick);
    }
    public double GetClosestTickAbove(double v)
    {
        return Tick * Math.Ceiling(v / Tick);
    }

}
With ability to create an instance ,but if you just want calculate and throw it away:   
double tickX = new AxisAssists(aMaxX - aMinX, 8).Tick;
2
Gregor Schmitz

J'ai écrit une méthode objective-c pour renvoyer une échelle d'axe de Nice et des graduations de Nice pour des valeurs min et max données de votre ensemble de données:

- (NSArray*)niceAxis:(double)minValue :(double)maxValue
{
    double min_ = 0, max_ = 0, min = minValue, max = maxValue, power = 0, factor = 0, tickWidth, minAxisValue = 0, maxAxisValue = 0;
    NSArray *factorArray = [NSArray arrayWithObjects:@"0.0f",@"1.2f",@"2.5f",@"5.0f",@"10.0f",nil];
    NSArray *scalarArray = [NSArray arrayWithObjects:@"0.2f",@"0.2f",@"0.5f",@"1.0f",@"2.0f",nil];

    // calculate x-axis Nice scale and ticks
    // 1. min_
    if (min == 0) {
        min_ = 0;
    }
    else if (min > 0) {
        min_ = MAX(0, min-(max-min)/100);
    }
    else {
        min_ = min-(max-min)/100;
    }

    // 2. max_
    if (max == 0) {
        if (min == 0) {
            max_ = 1;
        }
        else {
            max_ = 0;
        }
    }
    else if (max < 0) {
        max_ = MIN(0, max+(max-min)/100);
    }
    else {
        max_ = max+(max-min)/100;
    }

    // 3. power
    power = log(max_ - min_) / log(10);

    // 4. factor
    factor = pow(10, power - floor(power));

    // 5. Nice ticks
    for (NSInteger i = 0; factor > [[factorArray objectAtIndex:i]doubleValue] ; i++) {
        tickWidth = [[scalarArray objectAtIndex:i]doubleValue] * pow(10, floor(power));
    }

    // 6. min-axisValues
    minAxisValue = tickWidth * floor(min_/tickWidth);

    // 7. min-axisValues
    maxAxisValue = tickWidth * floor((max_/tickWidth)+1);

    // 8. create NSArray to return
    NSArray *niceAxisValues = [NSArray arrayWithObjects:[NSNumber numberWithDouble:minAxisValue], [NSNumber numberWithDouble:maxAxisValue],[NSNumber numberWithDouble:tickWidth], nil];

    return niceAxisValues;
}

Vous pouvez appeler la méthode comme ceci:

NSArray *niceYAxisValues = [self niceAxis:-maxy :maxy];

et obtenez votre configuration d'axe:

double minYAxisValue = [[niceYAxisValues objectAtIndex:0]doubleValue];
double maxYAxisValue = [[niceYAxisValues objectAtIndex:1]doubleValue];
double ticksYAxis = [[niceYAxisValues objectAtIndex:2]doubleValue];

Au cas où vous voudriez limiter le nombre de ticks d’axe, faites ceci:

NSInteger maxNumberOfTicks = 9;
NSInteger numberOfTicks = valueXRange / ticksXAxis;
NSInteger newNumberOfTicks = floor(numberOfTicks / (1 + floor(numberOfTicks/(maxNumberOfTicks+0.5))));
double newTicksXAxis = ticksXAxis * (1 + floor(numberOfTicks/(maxNumberOfTicks+0.5)));

La première partie du code est basée sur le calcul que j'ai trouvé ici pour calculer l’échelle de l’axe des graphes de Nice et les graduations similaires aux graphes Excel. Cela fonctionne très bien pour tout type de jeu de données. Voici un exemple d'implémentation iPhone:

enter image description here

2
JFS

Je suis l'auteur de " Algorithme pour une mise à l'échelle optimale sur un axe de graphique ". Auparavant, il était hébergé sur trollop.org, mais j'ai récemment déménagé des moteurs de domaines/blogs.

S'il vous plaît voir mon réponse à une question connexe .

1
Incongruous

Une autre idée est de faire en sorte que la plage de l’axe soit la plage des valeurs, mais placez les graduations à la position appropriée. C’est-à-dire que pour les positions 7 à 22:

 [- - - | - - - - | - - - - | - -] 
 10 15 20 

En ce qui concerne la sélection de l'espacement des tiques, je suggérerais un nombre quelconque de la forme 10 ^ x * i/n, où i <n et 0 <n <10. Générez cette liste et triez-la, vous pouvez trouver le plus grand inférieur à value_per_division (comme dans adam_liss) à l'aide d'une recherche binaire.

1
FryGuy

En python: steps = [numpy.round(x) for x in np.linspace(min, max, num=num_of_steps)]

0
MichaelLo

Si vous essayez de donner aux échelles une apparence correcte sur les graphiques VB.NET, alors j'ai utilisé l'exemple d'Adam Liss, mais assurez-vous que vous définissez les valeurs d'échelle min et max que vous les transmettez à partir d'une variable de type décimal. (pas de type simple ou double) sinon les valeurs de graduation finissent par être configurées comme suit: 8 décimales ..__ Ainsi, à titre d'exemple, j'avais un graphique dans lequel je définissais la valeur d'axe des ordonnées minimale à 0,0001 et à l'axe des ordonnées maximal valeur à 0,002 . Si je transmets ces valeurs à l'objet de graphique en tant que singles, j'obtiens des valeurs de graduation de 0,00048000001697801, 0.000860000036482233 .... Alors que si je transmets ces valeurs à l'objet de graphique en tant que décimales, j'obtiens Nice tick marquer des valeurs de 0,00048, 0,00086 ......

0
Kristian

Inspirant beaucoup des réponses déjà disponibles, voici mon implémentation en C. Notez qu’il existe une certaine extensibilité intégrée dans le tableau ndex.

float findNiceDelta(float maxvalue, int count)
{
    float step = maxvalue/count,
         order = powf(10, floorf(log10(step))),
         delta = (int)(step/order + 0.5);

    static float ndex[] = {1, 1.5, 2, 2.5, 5, 10};
    static int ndexLenght = sizeof(ndex)/sizeof(float);
    for(int i = ndexLenght - 2; i > 0; --i)
        if(delta > ndex[i]) return ndex[i + 1] * order;
    return delta*order;
}
0
Gleno

En R, utilisez

tickSize <- function(range,minCount){
    logMaxTick <- log10(range/minCount)
    exponent <- floor(logMaxTick)
    mantissa <- 10^(logMaxTick-exponent)
    af <- c(1,2,5) # allowed factors
    mantissa <- af[findInterval(mantissa,af)]
    return(mantissa*10^exponent)
}

où l'argument range est max-min du domaine.

0
Museful

Voici une fonction javascript que j'ai écrite pour arrondir les intervalles de grille (max-min)/gridLinesNumber à de belles valeurs. Cela fonctionne avec tous les nombres, voir le Gist avec des instructions détaillées pour savoir comment cela fonctionne et comment l’appeler.

var ceilAbs = function(num, to, bias) {
  if (to == undefined) to = [-2, -5, -10]
  if (bias == undefined) bias = 0
  var numAbs = Math.abs(num) - bias
  var exp = Math.floor( Math.log10(numAbs) )

    if (typeof to == 'number') {
        return Math.sign(num) * to * Math.ceil(numAbs/to) + bias
    }

  var mults = to.filter(function(value) {return value > 0})
  to = to.filter(function(value) {return value < 0}).map(Math.abs)
  var m = Math.abs(numAbs) * Math.pow(10, -exp)
  var mRounded = Infinity

  for (var i=0; i<mults.length; i++) {
    var candidate = mults[i] * Math.ceil(m / mults[i])
    if (candidate < mRounded)
      mRounded = candidate
  }
  for (var i=0; i<to.length; i++) {
    if (to[i] >= m && to[i] < mRounded)
      mRounded = to[i]
  }
  return Math.sign(num) * mRounded * Math.pow(10, exp) + bias
}

Appeler ceilAbs(number, [0.5]) pour des numéros différents arrondira les chiffres de la manière suivante:

301573431.1193228 -> 350000000
14127.786597236991 -> 15000
-63105746.17236853 -> -65000000
-718854.2201183736 -> -750000
-700660.340487957 -> -750000
0.055717507097870114 -> 0.06
0.0008068701205775142 -> 0.00085
-8.66660070605576 -> -9
-400.09256079792976 -> -450
0.0011740548815578223 -> 0.0015
-5.3003294346854085e-8 -> -6e-8
-0.00005815960629843176 -> -0.00006
-742465964.5184875 -> -750000000
-81289225.90985894 -> -85000000
0.000901771713513881 -> 0.00095
-652726598.5496342 -> -700000000
-0.6498901364393532 -> -0.65
0.9978325804695487 -> 1
5409.4078950583935 -> 5500
26906671.095639467 -> 30000000

Découvrez le violon pour expérimenter le code. Code dans la réponse, le Gist et le violon est légèrement différent J'utilise celui donné dans la réponse.

0
grabantot