Algoritmo de intervalos de línea de cuadrícula “agradables” en un gráfico

Necesito un algoritmo razonablemente inteligente para obtener líneas de cuadrícula “agradables” para un gráfico (gráfico).

Por ejemplo, suponga un gráfico de barras con valores de 10, 30, 72 y 60. Usted sabe:

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

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

El valor mínimo de la cuadrícula debe ser 0 o un valor “agradable” menor que el valor mínimo de los datos en el rango. Alternativamente, puede ser especificado.

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, puede querer de 0 a 100 si muestra 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 determinado (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 ejemplo, 80 sería un máximo sensible 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 necesito.

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

Ver también algoritmo Tickmark para un eje de gráfico

FYI, con sus datos de muestra:

  • Arce: Mín. = 8, Máx. = 74, Etiquetas = 10,20, .., 60,70, Garrapatas = 10,12,14, .. 70,72
  • MATLAB: Min = 10, Max = 80, Labels = 10,20 ,, .., 60,80

He hecho esto con un método de fuerza bruta. Primero, determine la cantidad máxima de marcas que puede caber en el espacio. Divida el rango total de valores por el número de tics; este es el espaciamiento mínimo de la marca. Ahora calcule el piso de la base del logaritmo 10 para obtener la magnitud de la marca, y divida por este valor. Debería terminar con algo en el rango de 1 a 10. Simplemente elija el número de ronda mayor o igual que el valor y multiplíquelo por el logaritmo calculado anteriormente. Este es tu espacio de tilde 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: puede alterar la selección de intervalos “agradables”. Un comentarista parece estar insatisfecho con las selecciones provistas, porque la cantidad real de ticks puede ser hasta 2.5 veces menor que el máximo. Aquí hay una pequeña modificación que define una tabla para los agradables intervalos. En el ejemplo, amplié las selecciones para que el número de ticks no sea inferior a 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 

Hay 2 piezas para el problema:

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

Puede manejar la primera parte usando logaritmos:

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

Entonces, por ejemplo, si su rango es de 50 a 1200, el exponente es 3 y la magnitud es 1000.

Luego, trate la segunda parte al decidir cuántas subdivisiones quiere en su grilla:

 value_per_division = magnitude / subdivisions; 

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

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

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

Escribí un método Object-c para devolver una escala de eje agradable y marcas agradables para los valores mínimo y máximo 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; } 

Puede llamar al método de esta manera:

 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 quiera limitar la cantidad de tics 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 gráfico agradable y los ticks similares a los gráficos de Excel. Funciona de manera excelente para todo tipo de conjuntos de datos. Aquí hay un ejemplo de una implementación de iPhone:

enter image description here

Tomado de Mark arriba, una clase Util ligeramente más completa en c #. Eso también calcula un primer y último tic adecuado.

 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; 

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

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

En cuanto a la selección del espaciado entre puntos, sugeriría cualquier número del formulario 10 ^ x * i / n, donde i

Soy el autor de ” Algoritmo para 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 .

Usando mucha inspiración de las respuestas que ya están disponibles aquí, aquí está mi implementación en C. Tenga en cuenta que hay cierta extensibilidad incorporada en la matriz 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; } 

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 de rango es max-min de dominio.

Aquí hay una función javascript que escribí para redondear los intervalos de cuadrícula (max-min)/gridLinesNumber a valores hermosos. Funciona con cualquier número, consulte la esencia con commets detallados para descubrir 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= m && to[i] < mRounded) mRounded = to[i] } return Math.sign(num) * mRounded * Math.pow(10, exp) + bias } 

Llamar a ceilAbs(number, [0.5]) para diferentes números redondeará números como ese:

 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 

Mira el violín para experimentar con el código. El código en la respuesta, la esencia y el violín es ligeramente diferente. Estoy usando el que se da en la respuesta.

Si está tratando de hacer que la balanza parezca correcta en los gráficos de VB.NET, entonces he usado el ejemplo de Adam Liss, pero asegúrese de que al establecer los valores de escala mínima y máxima los transfiera desde una variable de tipo decimal (no de tipo simple o doble), de lo contrario, los valores de la marca de verificación se configurarán como 8 decimales. Así que, como ejemplo, tuve 1 gráfico en el que establecí el valor mínimo del eje Y en 0.0001 y el valor máximo del eje Y en 0.002. Si paso estos valores al objeto del gráfico como solteros obtengo 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 unos buenos valores de marca de 0,00048, 0,00086 …. ..

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