algorithm - uso - tabla logaritmica base 10




Elegir una escala lineal atractiva para el eje Y de un gráfico (7)

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ántas marcas desea, 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);
?>

Resultado resultado de datos de muestra

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

Estoy escribiendo un poco de código para mostrar un gráfico de barras (o líneas) en nuestro software. Todo va bien Lo que me tiene perplejo es etiquetar el eje Y.

La persona que llama puede decirme cuán finamente quieren que se marque la escala Y, pero parece que estoy atascado en exactamente qué etiquetarlos de una manera "atractiva". No puedo describir "atractivo", y probablemente tampoco tú, pero lo sabemos cuando lo vemos, ¿verdad?

Entonces, si los puntos de datos son:

   15, 234, 140, 65, 90

Y el usuario pide 10 etiquetas en el eje Y, un poco de engaño con papel y lápiz se presenta 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 incremento "agradable" 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 que Excel aborda este problema muy bien.

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


Basado en el 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);
    }
}

La respuesta de 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:

  • Obtiene los extremos globales xMin y xMax (inluce todos los gráficos que quieras imprimir en el algoritmo)
  • calcular el rango entre xMin y xMax
  • calcule el orden de magnitud de su rango
  • calcule el tamaño de la garrapata dividiendo el rango por el número de garrapatas 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 tienes cero tick siempre impreso. Calcula el límite inferior y superior, pero recuerda centrar la trama

Empecemos. Primero 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);

Redondeo los valores mínimos y máximos para estar 100% seguro de que mi trazado 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 "tics agradables" para evitar tics como 7, 13, 17, etc. El método que uso 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 debes 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í hay métodos que mencioné antes, los cuales puede escribir usted mismo, pero también puede usar los míos

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

Solo hay una cosa más que no está incluida 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 eso ayude. ¡Aclamaciones!


Los algoritmos anteriores no toman en consideración el caso cuando el rango entre el valor mínimo y máximo 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 esos casos, escribí (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;
}

Parece que la persona que llama no le dice los rangos que quiere.

Por lo tanto, puede cambiar los puntos finales hasta que el recuento de etiquetas lo divida fácilmente.

Vamos a definir "agradable". Yo llamaría bueno si las etiquetas están apagadas 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, ...

Encuentra el máximo y el mínimo de tu serie de datos. Llamemos a estos puntos:

min_point and max_point.

Ahora todo lo que necesita hacer es encontrar 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 haya muchas soluciones, así que solo elija una. La mayoría de las veces apuesto a que puedes establecer

start_label to 0

así que intente un número entero diferente

end_label

hasta que el desplazamiento sea "bueno"


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

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




graph