Algorithme pour les intervalles de grille" nice " sur un graphique

j'ai besoin d'un algorithme raisonnablement intelligent pour trouver des lignes de grille" nice " pour un graphique (graphique).

par exemple, supposons un diagramme à barres avec des valeurs de 10, 30, 72 et 60. Vous savez:

valeur minimale: 10 Valeur Max: 72 Portée: 62

la première question est: par quoi commencez-vous? Dans ce cas, 0 serait la valeur intuitive mais cela ne tiendra pas sur les autres ensembles de données donc je devine:

grille valeur min doit être 0 ou un "gentil", valeur inférieure à la valeur min des données dans la gamme. Alternativement, il peut être spécifié.

grille valeur max doit être une valeur" nice " au-dessus de la valeur max dans la gamme. Alternativement, il peut être spécifié (par exemple, vous pourriez vouloir 0 à 100 Si vous montrez des pourcentages, indépendamment des valeurs réelles).

le nombre de lignes de quadrillage (tiques) dans l'intervalle doit être spécifié ou un nombre dans un intervalle donné (par exemple 3-8) ce que les valeurs sont "sympa" (c'est à dire des nombres ronds) et permet d'optimiser l'utilisation de la zone de graphique. Dans notre exemple, 80 correspondrait à un max raisonnable, puisque cela utiliserait 90% de la hauteur de la carte (72/80), tandis que 100 créerait plus d'espace perdu.

Quelqu'un connaît un bon algorithme pour ça? Le langage est hors de propos car je vais le mettre en œuvre dans ce dont j'ai besoin.

53
demandé sur cletus 2008-12-12 04:54:19

14 réponses

CPAN fournit une mise en œuvre ici (voir lien source)

Voir aussi algorithme de Tickmark pour un axe de graphique

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

  • Maple: Min=8, Max= 74, Labels=10,20,..,60, 70, Ticks=10,12,14,..70,72
  • MATLAB: Min=10, Max= 80, Labels=10,20,,..,60, 80
9
répondu Christoph Rüegg 2017-05-23 12:26:26

j'ai fait ça avec une sorte de méthode de la force brute. Tout d'abord, déterminez le nombre maximum de marques de graduation vous pouvez adapter dans l'espace. Divisez l'intervalle total des valeurs par le nombre de tiques; c'est le minimum espacement de la tique. Maintenant, calculez le plancher du logarithme base 10 pour obtenir la grandeur de la tique, et divisez par cette valeur. Vous devriez vous retrouver avec quelque chose dans la gamme de 1 à 10. Il suffit de choisir le nombre rond supérieur ou égal à la valeur et la multiplier par le logarithme calculé plus tôt. C'est votre dernier tick de l'espacement.

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 de "gentil" intervalles. Un commentateur semble insatisfait des sélections fournies, car le nombre réel de tiques peut être jusqu'à 2,5 fois inférieur au maximum. Voici une légère modification qui définit une table pour la belle intervalles. Dans l'exemple, j'ai Elargissement des sélections pour que le nombre de tiques 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
répondu Mark Ransom 2016-08-01 21:25:53

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

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

vous pouvez manipuler 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 gamme est de 50 - 1200, l'exposant est 3 et la magnitude est 1000.

puis traiter la deuxième partie en décidant comment beaucoup de subdivisions que vous voulez dans votre grille:

value_per_division = magnitude / subdivisions;

c'est un calcul approximatif parce que l'exposant a été tronqué à un entier. Vous pouvez vouloir modifier le calcul de l'exposant pour mieux gérer les conditions limites, par exemple en arrondissant au lieu de prendre le int() si vous finissez avec trop de subdivisions.

27
répondu Adam Liss 2008-12-12 02:09:48

j'utilise l'algorithme suivant. C'est similaire à d'autres affiché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
répondu Drew Noakes 2017-05-19 20:03:47

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;
};
4
répondu Drew Noakes 2017-05-19 20:03:33

j'ai écrit une méthode objective-c pour retourner une échelle d'axe de nice et de bonnes tiques 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 obtenir la configuration de l'axe:

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

Juste au cas où vous souhaitez limiter le nombre d'axes tiques faire 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 du graphique de nice et des tiques similaires aux graphiques excel. Il fonctionne excellent pour tous les types d'ensembles de données. Voici un exemple d'application iPhone:

enter image description here

2
répondu JFS 2014-04-11 05:45:19

tiré de la marque ci-dessus, une classe D'Util légèrement plus complète en c#. Cela calcule également une première et dernière tique appropriée.

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
répondu Gregor Schmitz 2014-10-01 16:36:05

une autre idée est d'avoir la portée de l'axe soit la portée des valeurs, mais de mettre les marques de tique à la position appropriée.. i.e. pour 7 à 22 do:

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

quant à la sélection de l'espacement des tiques, je suggérerais n'importe quel nombre de la forme 10^x * i / n, Où i < n, et 0 < n < 10. Générez cette liste, et triez-les, et vous pouvez trouver le plus grand nombre plus petit que value_per_division (comme dans adam_liss) en utilisant une recherche binaire.

1
répondu FryGuy 2008-12-12 02:22:42

je suis l'auteur de " algorithme pour une mise à L'échelle optimale sur un axe de carte ". Il était hébergé sur trollop.org, mais j'ai récemment déménagé domaines/blogging moteurs.

s'il vous Plaît voir mon réponse à une question relative à la .

1
répondu Incongruous 2017-05-23 12:34:38

en utilisant beaucoup d'inspiration des réponses déjà disponibles ici, voici mon implémentation en C. notez qu'il y a 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
répondu Gleno 2013-08-05 01:38:10

dans R, utiliser

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 de domaine.

0
répondu Museful 2013-10-04 14:04:37

Voici une fonction javascript que j'ai écrite pour arrondir les intervalles de grille (max-min)/gridLinesNumber à de belles valeurs. Il fonctionne avec tous les numéros, voir le gist avec des commets détaillés pour savoir comment il 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
}

appelant ceilAbs(number, [0.5]) pour différents nombres arrondira les nombres comme cela:

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

consultez le violon pour expérimenter avec le code. Code dans la réponse, l'essentiel et le violon sont légèrement différents j'utilise celui donné dans la réponse.

0
répondu grabantot 2016-02-07 13:22:26

si vous essayez d'obtenir les échelles à droite sur VB.NET graphiques, puis j'ai utilisé L'exemple D'Adam Liss, mais assurez-vous lorsque vous définissez les valeurs d'échelle min et max que vous passez à partir d'une variable de type décimal (pas de type simple ou double) sinon les valeurs de marque de tique finissent par être réglé à comme 8 décimales. Ainsi, par exemple, j'ai eu 1 graphique où j'ai mis la valeur de l'axe min Y à 0.0001 et la valeur de l'axe max Y à 0.002. Si je passe ces valeurs à l'objet graphique comme singles-je obtenir graduation des valeurs de 0.00048000001697801, 0.000860000036482233 .... Alors que si je passe ces valeurs à l'objet graphique en décimales, j'obtiens de belles valeurs de marque de 0.00048, 0.00086 ......

0
répondu Kristian 2016-04-08 12:44:37

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

0
répondu MichaelLo 2018-01-01 20:29:30