2008-11-28 26 views
59

Estoy escribiendo un poco de código para mostrar una barra (o línea) gráfico en nuestro software. Todo va bien Lo que me tiene perplejo es etiquetar el eje Y.La elección de una escala lineal atractivo para un gráfico de eje Y

La persona que llama puede decirme cómo finamente quieren que la escala Y etiquetados, pero parecen estar atrapados en exactamente lo que etiquetarlos en un tipo "atractiva" del camino. No puedo describir "atractivo", y probablemente tampoco tú, pero lo sabemos cuando lo vemos, ¿verdad?

lo tanto, si los puntos de datos son:

15, 234, 140, 65, 90 

Y el usuario pide 10 etiquetas en el eje Y, un poco de artimañas con papel y lápiz viene con:

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

Así que hay 10 allí (sin incluir 0), el último se extiende un poco más allá del valor más alto (234 < 250), y es un "buen" incremento de 25 cada uno. Si pidieran 8 etiquetas, un incremento de 30 se habría visto bien:

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

Nueve habría sido complicado. Tal vez solo haya usado 8 o 10 y llamarlo lo suficientemente cerca estaría bien. ¿Y qué hacer cuando algunos de los puntos son negativos?

puedo ver aborda este problema de Excel muy bien.

¿Alguien sabe un algoritmo de propósito general (incluso algunos fuerza bruta está bien) para resolver esto? No tengo que hacerlo rápido, pero debería verse bien.

+2

Una muy buena pregunta. –

+1

Hay información sobre cómo Excel elige los valores máximos y mínimos para su eje Y aquí: http://support.microsoft.com/kb/214075 –

+0

Implementación agradable: http://stackoverflow.com/a/16363437/829571 – assylias

Respuesta

84

mío, hace mucho tiempo O haber escrito un gráfico módulo que cubrió esto muy bien.La excavación en la masa gris obtiene lo siguiente:

  • Determinación del límite superior e inferior de los datos. (Cuidado con el caso especial en el límite inferior = límite superior!
  • gama Dividir en la cantidad necesaria de las garrapatas.
  • Ronda de la garrapata intervalo hasta en cantidades agradables.
  • Ajuste el límite inferior y superior en consecuencia.

Permite tomar su ejemplo:

15, 234, 140, 65, 90 with 10 ticks 
  1. límite inferior = 15
  2. límite superior = 234
  3. rango = 234-15 = 219
  4. marque el rango = 21.9. Esto debería ser 25,0
  5. nuevo límite inferior = 25 * ronda (15/25) = 0
  6. nuevo límite superior = 25 * redondo (1 + 235/25) = 250

lo que el rango = 0,25,50, ..., 225.250

Usted puede obtener el rango de garrapata agradable con los siguientes pasos:

  1. dividir por 10^x tal que el resultado se encuentra entre 0.1 y 1.0 (incluyendo 0.1 excluyendo 1).
  2. traducir en consecuencia:
    • 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. multiplicar por 10^x.

En este caso, 21.9 se divide entre 10^2 para obtener 0.219. Esto es < = 0.25, así que ahora tenemos 0.25. Multiplicado por 10^2 esto da 25.

permite echar un vistazo al mismo ejemplo con 8 garrapatas:

unido
15, 234, 140, 65, 90 with 8 ticks 
  1. inferior = 15
  2. límite superior = 234
  3. rango = 234-15 = 219
  4. garrapata rango = 27.375
    1. Divida por 10^2 para 0.27375, se traduce en 0.3, que da (multipli ed por 10^2) 30.
  5. nuevo límite inferior = 30 * ronda (15/30) = 0
  6. nuevo límite superior = 30 * redondo (1 + 235/30) = 240

que dan el resultado que ha solicitado ;-).

------ ------ añadida por KD

Aquí está el código que logra este algoritmo sin necesidad de utilizar tablas de búsqueda, 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 términos generales, la el número de tics incluye el tic del fondo, por lo que los segmentos reales del eje y son uno menos que el número de tics.

+1

Esto era casi correcto. Paso 3, tuve que reducir X en 1. Para obtener un rango de 219 a .1-> 1, tengo que dividir entre 10^3 (1000) y no entre 10^2 (100). De lo contrario, véalo. –

+2

Hace referencia a dividir entre 10^x y multiplicar por 10^x. Cabe señalar que x se puede encontrar de esta manera: 'double x = Math.Ceiling (Math.Log10 (tickRange));' – Bryan

+5

Permítanme aprovechar esta oportunidad para decir "Te Amo Señor" ... –

5

suena como la persona que llama no le dice a los rangos que desea.

Así que son libres cambiado los criterios de valoración hasta que hacerlo bien divisible por el recuento de etiqueta.

Definamos "agradable". Me llamo bueno si las etiquetas están apagados por:

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, ... 

Halla el máximo y el mínimo de la serie de datos. Vamos a llamar a estos puntos:

min_point and max_point. 

Ahora todo lo que necesita hacer es encontrar es de 3 valores:

- 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" 

que se ajustan a la ecuación:

(end_label - start_label)/label_offset == label_count 

Probablemente hay muchas soluciones, por lo solo elige uno. La mayoría de las veces me apuesta que se puede establecer

start_label to 0 

tan sólo tratar de diferente número entero

end_label 

hasta que el desplazamiento es "agradable"

0

Gracias por la pregunta y la respuesta, muy útil. Gamecat, me pregunto cómo estás determinando a qué se debe redondear el rango de tics.

marque el rango = 21.9. Esto debería ser 25.0

Para hacer esto algorítmicamente, uno tendría que agregar la lógica al algoritmo anterior para hacer esta escala muy bien para números más grandes? Por ejemplo, con 10 ticks, si el rango es 3346, el rango de ticks se evaluaría a 334.6 y el redondeo al 10 más cercano daría 340 cuando 350 es probablemente mejor.

¿Qué opinas?

+0

En el ejemplo de @ Gamecat, 334.6 => 0.3346, que debería ir a 0.4. Entonces, el rango de tic sería en realidad 400, que es un número bastante bueno. – Bryan

7

Pruebe este código. Lo he usado en algunos escenarios de gráficos y funciona bien. También es bastante rápido.

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

Aquí hay un ejemplo de PHP que estoy usando. Esta función devuelve una matriz de valores bonitos del eje Y que abarcan los valores mínimo y máximo Y pasados. Por supuesto, esta rutina también se podría usar para los valores del eje X.

Le permite "sugerir" cuántos ticks puede querer, pero la rutina devolverá lo que se ve bien. He agregado algunos datos de muestra y he mostrado los resultados de estos.

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

de salida resultado de datos de ejemplo

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

¡Rock! tener un voto positivo! – isaac9A

+0

mi jefe estará contento con esto - voto de mi parte también n ¡GRACIAS! –

1

esto funciona como un encanto, si quieres 10 pasos + cero

//get proper scale for y 
$maximoyi_temp= max($institucion); //get max value from data array 
for ($i=10; $i< $maximoyi_temp; $i=($i*10)) { 
    if (($divisor = ($maximoyi_temp/$i)) < 2) break; //get which divisor will give a number between 1-2  
} 
$factor_d = $maximoyi_temp/$i; 
$factor_d = ceil($factor_d); //round up number to 2 
$maximoyi = $factor_d * $i; //get new max value for y 
if (($maximoyi/ $maximoyi_temp) > 2) $maximoyi = $maximoyi /2; //check if max value is too big, then split by 2 
3

Todavía estoy luchando con esto :)

La respuesta original de Gamecat parece funcionar la mayor parte del tiempo, pero intente conectar, por ejemplo, "3 ticks" como el número de t icks requeridos (para los mismos valores de datos 15, 234, 140, 65, 90) ... parece dar un rango de tic de 73, que después de dividir por 10^2 rinde 0,73, que se asigna a 0,75, lo que da una 'agradable' gama garrapata de 75.

luego calculando límite superior: 75 * redondo (1 + 234/75) = 300

y el límite inferior: 75 * ronda (15/75) = 0

Pero claramente si comienzas en 0, y sigues en pasos de 75 hasta el límite superior de 300, terminas con 0,75,150,225,300 .... que sin duda es útil, pero son 4 ticks (no incluyendo 0) no los 3 tics requeridos.

Es frustrante que no funcione el 100% del tiempo ... lo cual podría deberse a mi error en alguna parte, por supuesto.

+0

Originalmente se pensó que el problema podría tener algo que ver con el método sugerido por Bryan para derivar x, pero esto por supuesto es perfectamente exacto. – StillPondering

0

Basado en @ algoritmo de Gamecat, produje la siguiente clase de ayuda

public struct Interval 
{ 
    public readonly double Min, Max, TickRange; 

    public static Interval Find(double min, double max, int tickCount, double padding = 0.05) 
    { 
     double range = max - min; 
     max += range*padding; 
     min -= range*padding; 

     var attempts = new List<Interval>(); 
     for (int i = tickCount; i > tickCount/2; --i) 
      attempts.Add(new Interval(min, max, i)); 

     return attempts.MinBy(a => a.Max - a.Min); 
    } 

    private Interval(double min, double max, int tickCount) 
    { 
     var candidates = (min <= 0 && max >= 0 && tickCount <= 8) ? new[] {2, 2.5, 3, 4, 5, 7.5, 10} : new[] {2, 2.5, 5, 10}; 

     double unroundedTickSize = (max - min)/(tickCount - 1); 
     double x = Math.Ceiling(Math.Log10(unroundedTickSize) - 1); 
     double pow10X = Math.Pow(10, x); 
     TickRange = RoundUp(unroundedTickSize/pow10X, candidates) * pow10X; 
     Min = TickRange * Math.Floor(min/TickRange); 
     Max = TickRange * Math.Ceiling(max/TickRange); 
    } 

    // 1 < scaled <= 10 
    private static double RoundUp(double scaled, IEnumerable<double> candidates) 
    { 
     return candidates.First(candidate => scaled <= candidate); 
    } 
} 
0

Los algoritmos anteriores no tienen en consideración el caso cuando el intervalo entre min y max valor es demasiado pequeño. ¿Y qué pasa si estos valores son mucho más altos que cero? Entonces, tenemos la posibilidad de comenzar el eje y con un valor superior a cero. Además, para evitar que nuestra línea esté completamente en el lado superior o inferior del gráfico, debemos darle un poco de "aire para respirar".

para cubrir los casos que he escrito (en PHP) el código anterior:

function calculateStartingPoint($min, $ticks, $times, $scale) { 

    $starting_point = $min - floor((($ticks - $times) * $scale)/2); 

    if ($starting_point < 0) { 
     $starting_point = 0; 
    } else { 
     $starting_point = floor($starting_point/$scale) * $scale; 
     $starting_point = ceil($starting_point/$scale) * $scale; 
     $starting_point = round($starting_point/$scale) * $scale; 
    } 
    return $starting_point; 
} 

function calculateYaxis($min, $max, $ticks = 7) 
{ 
    print "Min = " . $min . "\n"; 
    print "Max = " . $max . "\n"; 

    $range = $max - $min; 
    $step = floor($range/$ticks); 
    print "First step is " . $step . "\n"; 
    $available_steps = array(5, 10, 20, 25, 30, 40, 50, 100, 150, 200, 300, 400, 500); 
    $distance = 1000; 
    $scale = 0; 

    foreach ($available_steps as $i) { 
     if (($i - $step < $distance) && ($i - $step > 0)) { 
      $distance = $i - $step; 
      $scale = $i; 
     } 
    } 

    print "Final scale step is " . $scale . "\n"; 

    $times = floor($range/$scale); 
    print "range/scale = " . $times . "\n"; 

    print "floor(times/2) = " . floor($times/2) . "\n"; 

    $starting_point = calculateStartingPoint($min, $ticks, $times, $scale); 

    if ($starting_point + ($ticks * $scale) < $max) { 
     $ticks += 1; 
    } 

    print "starting_point = " . $starting_point . "\n"; 

    // result calculation 
    $result = []; 
    for ($x = 0; $x <= $ticks; $x++) { 
     $result[] = $starting_point + ($x * $scale); 
    } 
    return $result; 
} 
1

La respuesta por Toon Krijthe Cómo funciona la mayor parte del tiempo. Pero a veces producirá un exceso de garrapatas. No funcionará con números negativos también. El enfoque general del problema está bien, pero hay una mejor manera de manejar esto. El algoritmo que desee utilizar dependerá de lo que realmente desee obtener. A continuación te presento mi código que utilicé en mi biblioteca JS Ploting. Lo probé y siempre funciona (con suerte;)). Estos son los pasos principales:

  • obtener globales Extremas xmin y xmax (inlucde todas las parcelas que desea imprimir en el algoritmo) rango
  • calcular entre xMin y xMax
  • calcular el orden de magnitud de su rango
  • calcule el tamaño de marca dividiendo el rango por el número de ticks menos uno
  • este es opcional. Si desea que la marca cero esté siempre impresa, use el tamaño de tilde para calcular la cantidad de tics positivos y negativos. El número total de tics será su suma + 1 (la marca del cero)
  • , este no es necesario si tiene cero tick siempre impreso.Calcule el límite inferior y superior, pero recuerde centrar el gráfico

Comencemos. En primer lugar los cálculos básicos

var range = Math.abs(xMax - xMin); //both can be negative 
    var rangeOrder = Math.floor(Math.log10(range)) - 1; 
    var power10 = Math.pow(10, rangeOrder); 
    var maxRound = (xMax > 0) ? Math.ceil(xMax/power10) : Math.floor(xMax/power10); 
    var minRound = (xMin < 0) ? Math.floor(xMin/power10) : Math.ceil(xMin/power10); 

que completan los valores mínimo y máximo para estar 100% seguro de que mi parcela cubrirá todos los datos. También es muy importante registrar el piso10 del rango si es negativo o no y restar 1 más tarde. De lo contrario, su algoritmo no funcionará para números menores a uno.

var fullRange = Math.abs(maxRound - minRound); 
    var tickSize = Math.ceil(fullRange/(this.XTickCount - 1)); 

    //You can set nice looking ticks if you want 
    //You can find exemplary method below 
    tickSize = this.NiceLookingTick(tickSize); 

    //Here you can write a method to determine if you need zero tick 
    //You can find exemplary method below 
    var isZeroNeeded = this.HasZeroTick(maxRound, minRound, tickSize); 

utilizo "garrapatas con buena pinta" para evitar las garrapatas como 7, 13, 17, etc método que utilizo aquí es bastante simple. También es bueno tener zeroTick cuando sea necesario. Parcela se ve mucho más profesional de esta manera. Encontrarás todos los métodos al final de esta respuesta.

Ahora debe calcular los límites superior e inferior. Esto es muy fácil con cero tics, pero requiere un poco más de esfuerzo en otro caso. ¿Por qué? Porque queremos centrar la trama dentro del límite superior e inferior muy bien. Eche un vistazo a mi código. Algunas de las variables se definen fuera de este ámbito y algunas de ellas son propiedades de un objeto en el que se guarda el código completo presentado.

if (isZeroNeeded) { 

     var positiveTicksCount = 0; 
     var negativeTickCount = 0; 

     if (maxRound != 0) { 

      positiveTicksCount = Math.ceil(maxRound/tickSize); 
      XUpperBound = tickSize * positiveTicksCount * power10; 
     } 

     if (minRound != 0) { 
      negativeTickCount = Math.floor(minRound/tickSize); 
      XLowerBound = tickSize * negativeTickCount * power10; 
     } 

     XTickRange = tickSize * power10; 
     this.XTickCount = positiveTicksCount - negativeTickCount + 1; 
    } 
    else { 
     var delta = (tickSize * (this.XTickCount - 1) - fullRange)/2.0; 

     if (delta % 1 == 0) { 
      XUpperBound = maxRound + delta; 
      XLowerBound = minRound - delta; 
     } 
     else { 
      XUpperBound = maxRound + Math.ceil(delta); 
      XLowerBound = minRound - Math.floor(delta); 
     } 

     XTickRange = tickSize * power10; 
     XUpperBound = XUpperBound * power10; 
     XLowerBound = XLowerBound * power10; 
    } 

Y aquí están los métodos que he mencionado antes, que se puede escribir por sí mismo, pero también puede utilizar la mina

this.NiceLookingTick = function (tickSize) { 

    var NiceArray = [1, 2, 2.5, 3, 4, 5, 10]; 

    var tickOrder = Math.floor(Math.log10(tickSize)); 
    var power10 = Math.pow(10, tickOrder); 
    tickSize = tickSize/power10; 

    var niceTick; 
    var minDistance = 10; 
    var index = 0; 

    for (var i = 0; i < NiceArray.length; i++) { 
     var dist = Math.abs(NiceArray[i] - tickSize); 
     if (dist < minDistance) { 
      minDistance = dist; 
      index = i; 
     } 
    } 

    return NiceArray[index] * power10; 
} 

this.HasZeroTick = function (maxRound, minRound, tickSize) { 

    if (maxRound * minRound < 0) 
    { 
     return true; 
    } 
    else if (Math.abs(maxRound) < tickSize || Math.round(minRound) < tickSize) { 

     return true; 
    } 
    else { 

     return false; 
    } 
} 

Sólo hay una cosa más que no se incluye aquí. Este es el "buen aspecto de los límites". Estos son límites inferiores que son números similares a los números en "ticks agradables". Por ejemplo, es mejor tener el límite inferior comenzando en 5 con el tamaño de tilde 5 que tener un gráfico que comienza en 6 con el mismo tamaño de tilde. Pero este mi despedido te lo dejo a ti.

Espero que ayude. ¡Salud!

Cuestiones relacionadas