Algoritmo para intervalos de líneas de cuadrícula "agradables" en un gráfico


Necesito un algoritmo razonablemente inteligente para crear líneas de cuadrícula "agradables" para un gráfico (gráfico).

Por ejemplo, supongamos un gráfico de barras con valores de 10, 30, 72 y 60. Sabes:

Valor mínimo: 10 Valor máximo: 72 Rango: 62

La primera pregunta es: ¿de qué empiezas? En este caso, 0 sería el valor intuitivo, pero esto no se sostendrá en otros conjuntos de datos, así que supongo:

El valor mínimo de la cuadrícula debe ser 0 o un valor" agradable " menor que el valor mínimo del datos en rango. Alternativamente, se puede especificar.

El valor máximo de la cuadrícula debe ser un valor "agradable" por encima del valor máximo en el rango. Alternativamente, se puede especificar (por ejemplo, es posible que desee 0 a 100 si está mostrando porcentajes, independientemente de los valores reales).

El número de líneas de cuadrícula (marcas) en el rango debe especificarse o un número dentro de un rango dado (por ejemplo, 3-8) de modo que los valores sean "agradables" (es decir, números redondos) y maximice el uso del área del gráfico. En nuestro por ejemplo, 80 sería un máximo razonable, ya que usaría el 90% de la altura del gráfico (72/80), mientras que 100 crearía más espacio desperdiciado.

Alguien sabe de un buen algoritmo para esto? El lenguaje es irrelevante ya que lo implementaré en lo que necesite.

Author: cletus, 2008-12-12

14 answers

CPAN proporciona una implementación aquí (ver enlace fuente)

Véase también Algoritmo de marca de verificación para un eje de gráfico

Para su información, con sus datos de muestra:

  • Arce: Min=8, Max=74, Etiquetas=10,20,..,60,70, Garrapatas=10,12,14,..70,72
  • MATLAB: Min = 10, Max=80, Labels=10,20,,..,60,80
 9
Author: Christoph Rüegg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:26:26

He hecho esto con una especie de método de fuerza bruta. Primero, averigüe el número máximo de marcas que puede caber en el espacio. Divida el rango total de valores por el número de marcas; este es el espaciado mínimo de la marca. Ahora calcular el piso del logaritmo base 10 para obtener la magnitud de la garrapata, y dividir por este valor. Usted debe terminar con algo en el rango de 1 a 10. Simplemente elija el número redondo mayor o igual al valor y multiplíquelo por el logaritmo calculado anteriormente. Este es tu espacio final.

Ejemplo 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

Editar: usted es libre de alterar la selección de intervalos "agradables". Un comentarista parece no estar satisfecho con las selecciones proporcionadas, porque el número real de marcas puede ser hasta 2.5 veces menor que el máximo. Aquí hay una ligera modificación que define una tabla para los intervalos agradables. En el ejemplo, he expandido las selecciones para que el número de marcas no sea menor que 3/5 del máximo.

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
Author: Mark Ransom,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-01 21:25:53

Hay 2 piezas para el problema:

  1. Determinar el orden de magnitud involucrado, y
  2. Redondea a algo conveniente.

Puedes manejar la primera parte usando logaritmos:

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

Así, por ejemplo, si su rango es de 50 - 1200, el exponente es 3 y la magnitud es 1000.

Luego trate con la segunda parte decidiendo cuántas subdivisiones desea en su cuadrícula:

value_per_division = magnitude / subdivisions;

Este es un cálculo aproximado porque el exponente tiene ha sido truncado a un entero. Es posible que desee ajustar el cálculo del exponente para manejar mejor las condiciones de contorno, por ejemplo, redondeando en lugar de tomar el int() si termina con demasiadas subdivisiones.

 27
Author: Adam Liss,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-12 02:09:48

Utilizo el siguiente algoritmo. Es similar a otros publicados aquí, pero es el primer ejemplo 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
Author: Drew Noakes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-19 20:03:47

Aquí hay otra implementación 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
Author: Drew Noakes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-19 20:03:33

Escribí un método objective-c para devolver una escala de eje agradable y ticks agradables para valores mínimos y máximos dados de su conjunto de datos:

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

Puedes llamar al método así:

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

Y obtener la configuración del eje:

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

En caso de que desee limitar el número de marcas de eje, haga lo siguiente:

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 primera parte del código se basa en el cálculo que encontré aquí para calcular la escala del eje del gráfico de Niza y las garrapatas similares a los gráficos de excel. Funciona excelente para todo tipo de conjuntos de datos. Aquí hay un ejemplo de una implementación de iPhone:

introduzca la descripción de la imagen aquí

 2
Author: JFS,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-04-11 05:45:19

Tomado de la marca anterior, una clase Util ligeramente más completa en c#. Eso también calcula una primera y última garrapata adecuada.

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
Author: Gregor Schmitz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-01 16:36:05

Otra idea es que el rango del eje sea el rango de los valores, pero ponga las marcas de verificación en la posición apropiada.. es decir, para 7 a 22 do:

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

En cuanto a la selección del espaciado de garrapatas, sugeriría cualquier número de la forma 10^x * i / n, donde i

 1
Author: FryGuy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-12 02:22:42

Soy el autor de "Algoritmo para el Escalado Óptimo en un Eje de Gráfico". Solía estar alojado en trollop.org, pero recientemente he movido dominios / motores de blogs.

Por favor vea mi respuesta a una pregunta relacionada.

 1
Author: Incongruous,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:38

Usando mucha inspiración de respuestas ya disponibles aquí, aquí está mi implementación en C. Tenga en cuenta que hay cierta extensibilidad incorporada en el array 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
Author: Gleno,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-05 01:38:10

En R, use

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

Donde el argumento range es max-min de dominio.

 0
Author: Museful,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-10-04 14:04:37

Aquí hay una función javascript que escribí para redondear intervalos de cuadrícula (max-min)/gridLinesNumber a valores hermosos. Funciona con cualquier número, consulte el resumen con commets detallados para averiguar cómo funciona y cómo llamarlo.

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
}

Llamando a ceilAbs(number, [0.5]) para números diferentes redondeará números así:

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

Echa un vistazo al violín para experimentar con el código. Código en la respuesta, la esencia y el violín es ligeramente diferente Estoy usando el dado en la respuesta.

 0
Author: grabantot,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-07 13:22:26

Si usted está tratando de conseguir que la balanza se vea bien en VB.NET gráficos, luego he utilizado el ejemplo de Adam Liss, pero asegúrese de que cuando establezca los valores de escala min y max que los pase desde una variable de tipo decimal (no de tipo simple o doble) de lo contrario, los valores de marca de verificación terminan siendo configurados como 8 lugares decimales. Así como un ejemplo, tenía 1 gráfico donde establecí el valor mínimo del eje Y a 0.0001 y el valor máximo del eje Y a 0.002. Si paso estos valores al objeto del gráfico como simples I obtenga valores de marca de verificación de 0.00048000001697801, 0.000860000036482233 .... Mientras que si paso estos valores al objeto del gráfico como decimales obtengo buenos valores de marca de verificación de 0.00048, 0.00086 ......

 0
Author: Kristian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-08 12:44:37

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

 0
Author: MichaelLo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-01 20:29:30