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

84

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 dejó perplejo es etiquetar el eje Y.

La persona que llama puede decirme con qué precisión quieren que se etiquete la escala Y, pero parece que estoy atascado en exactamente qué etiquetarlos de una manera "atractiva". No puedo describir "atractivo", y probablemente tú tampoco, 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 manipulación con papel y lápiz se obtiene:

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

Entonces 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 conoce un algoritmo de propósito general (incluso algo de fuerza bruta está bien) para resolver esto? No tengo que hacerlo rápido, pero debería verse bien.

Clinton Pierce
fuente
1
Hay información sobre cómo Excel elige los valores máximo y mínimo para su eje Y aquí: support.microsoft.com/kb/214075
Christopher Orr
Buena implementación: stackoverflow.com/a/16363437/829571
assylias

Respuestas:

103

Hace mucho tiempo, escribí un módulo de gráficos que cubría esto muy bien. Excavar en la masa gris obtiene lo siguiente:

  • Determine el límite inferior y superior de los datos. (¡Tenga cuidado con el caso especial donde límite inferior = límite superior!
  • Divida el rango en la cantidad requerida de garrapatas.
  • Redondea el rango de garrapatas en buenas cantidades.
  • Ajuste el límite inferior y superior en consecuencia.

Tomemos tu 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. rango de tick = 21,9. Esto debería ser 25.0
  5. nuevo límite inferior = 25 * ronda (15/25) = 0
  6. nuevo límite superior = 25 * ronda (1 + 235/25) = 250

Entonces el rango = 0,25,50, ..., 225,250

Puede obtener el buen rango de ticks con los siguientes pasos:

  1. dividir por 10 ^ x de modo que el resultado esté entre 0,1 y 1,0 (incluido 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 por 10 ^ 2 para obtener 0,219. Esto es <= 0,25, por lo que ahora tenemos 0,25. Multiplicado por 10 ^ 2, da 25.

Echemos un vistazo al mismo ejemplo con 8 ticks:

15, 234, 140, 65, 90 with 8 ticks
  1. límite inferior = 15
  2. límite superior = 234
  3. rango = 234-15 = 219
  4. rango de tick = 27,375
    1. Dividir por 10 ^ 2 para obtener 0,27375, se traduce en 0,3, lo que da (multiplicado por 10 ^ 2) 30.
  5. nuevo límite inferior = 30 * ronda (15/30) = 0
  6. nuevo límite superior = 30 * ronda (1 + 235/30) = 240

Que dan el resultado que solicitaste ;-).

------ Agregado por KD ------

Aquí está el código que logra este algoritmo sin usar 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 cantidad de marcas incluye la marca inferior, por lo que los segmentos reales del eje y son uno menos que la cantidad de marcas.

Toon Krijthe
fuente
1
Esto estaba bien. Paso 3, tuve que reducir X en 1. Para obtener un rango de 219 a .1-> 1 tengo que dividir entre 10 ^ 3 (1000) no 10 ^ 2 (100). De lo contrario, acertar.
Clinton Pierce
2
Hace referencia a dividir por 10 ^ xy multiplicar por 10 ^ x. Cabe señalar que x se puede encontrar de esta manera: 'doble x = Math.Ceiling (Math.Log10 (tickRange));'
Bryan
1
Muy útil. Aunque no entendí - 'nuevo límite inferior = 30 * ronda (15/30) = 0' (Vendrá 30, creo) y cómo obtuviste 235 en 'nuevo límite superior = 30 * ronda (1 + 235/30) = 240 '235 no se menciona en ninguna parte, debería ser 234.
Mutante
4
Esta es una respuesta genial. Muy apreciado.
Joel Anair
4
@JoelAnair Gracias, acabas de hacer un día triste un poco más brillante.
Toon Krijthe
22

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 Y mínimo y máximo pasados. Por supuesto, esta rutina también podría usarse para valores del eje X.

Le permite "sugerir" cuántos tics puede querer, pero la rutina devolverá lo que se ve bien. Agregué algunos datos de muestra y mostré los resultados para 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);
?>

Salida de 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
)
Scott Guthrie
fuente
mi jefe estará contento con esto - ¡¡también un voto a favor mío n GRACIAS !!
Stephen Hazel
¡Gran respuesta! Lo convierto a Swift 4 stackoverflow.com/a/55151115/2670547
Petr Syrov
@Scott Guthrie: Esto es genial a menos que las entradas no sean enteros y sean números pequeños, por ejemplo, si yMin = 0.03 y yMax = 0.11.
Greg
9

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;
    }
}
Dibujó Noakes
fuente
6

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

Por lo tanto, puede cambiar los puntos finales hasta que sea divisible por el recuento de etiquetas.

Definamos "agradable". Llamaría agradable si las etiquetas están desactivadas 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, ...

Encuentre el máximo y el mínimo de su 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 ajusta a la ecuación:

(end_label - start_label)/label_offset == label_count

Probablemente haya muchas soluciones, así que elija una. La mayor parte del tiempo apuesto a que puedes configurar

start_label to 0

así que prueba un entero diferente

end_label

hasta que el desplazamiento sea "agradable"

Pirolístico
fuente
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 ticks requerido (para los mismos valores de datos 15, 234, 140, 65, 90) .... parece dar un rango de ticks de 73, que después de dividir por 10 ^ 2 da 0,73, que se asigna a 0,75, lo que da un rango de ticks 'agradable' de 75.

Luego calculando el límite superior: 75 * round (1 + 234/75) = 300

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

Pero claramente si comienzas en 0 y continúas en pasos de 75 hasta el límite superior de 300, terminas con 0,75,150,225,300 ... lo que sin duda es útil, pero son 4 tics (sin incluir 0) no el Se requieren 3 tics.

Simplemente frustrante porque no funciona el 100% del tiempo ... ¡lo que podría deberse a mi error en algún lugar, por supuesto!

Todavía reflexionando
fuente
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
3

La respuesta de Toon Krijthe funciona la mayor parte del tiempo. Pero a veces producirá un número excesivo de garrapatas. Tampoco funcionará con números negativos. El enfoque general del problema está bien, pero hay una mejor manera de manejarlo. El algoritmo que desee utilizar dependerá de lo que realmente desee obtener. A continuación, les presento mi código que usé en mi biblioteca JS Ploting. Lo he probado y siempre funciona (con suerte;)). Estos son los pasos principales:

  • obtenga los extremos globales xMin y xMax (incluya todos los gráficos que desea imprimir en el algoritmo)
  • calcular el rango entre xMin y xMax
  • calcula el orden de magnitud de tu rango
  • Calcule el tamaño del tick dividiendo el rango por el número de ticks menos uno
  • este es opcional. Si desea tener cero tick siempre impreso, use el tamaño de tick para calcular el número de ticks positivos y negativos. El número total de ticks será su suma + 1 (el tick cero)
  • este no es necesario si tiene cero tick siempre impreso. Calcule el límite inferior y superior, pero recuerde centrar la gráfica

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ínimo y máximo para estar 100% seguro de que mi gráfico cubrirá todos los datos. También es muy importante registrar el piso 10 del rango si es negativo o no y restar 1 más tarde. De lo contrario, su algoritmo no funcionará para números menores que 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 bonitas" para evitar garrapatas como 7, 13, 17, etc. El método que utilizo aquí es bastante simple. También es bueno tener zeroTick cuando sea necesario. La trama se ve mucho más profesional de esta manera. Encontrará todos los métodos al final de esta respuesta.

Ahora tienes que calcular los límites superior e inferior. Esto es muy fácil con cero tick pero requiere un poco más de esfuerzo en otro caso. ¿Por qué? Porque queremos centrar bien la trama dentro de los límites superior e inferior. 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 todo el código 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 que puede escribir usted mismo pero también puede usar el mío

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í. Estos son los "lindos límites". Estos son límites inferiores que son números similares a los números de "tics bonitos". Por ejemplo, es mejor tener el límite inferior comenzando en 5 con un tamaño de tick 5 que tener una gráfica que comience en 6 con el mismo tamaño de tick. Pero este mi despedido te lo dejo.

Espero eso ayude. ¡Salud!

Arturo
fuente
2

Convirtió esta respuesta como Swift 4

extension Int {

    static func makeYaxis(yMin: Int, yMax: Int, ticks: Int = 10) -> [Int] {
        var yMin = yMin
        var yMax = yMax
        var ticks = ticks
        // 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.
        var result = [Int]()
        // 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 -= ticks   // some small value
            yMax += ticks   // some small value
        }
        // Determine Range
        let range = yMax - yMin
        // Adjust ticks if needed
        if ticks < 2 { ticks = 2 }
        else if ticks > 2 { ticks -= 2 }

        // Get raw step value
        let tempStep: CGFloat = CGFloat(range) / CGFloat(ticks)
        // Calculate pretty step value
        let mag = floor(log10(tempStep))
        let magPow = pow(10,mag)
        let magMsd = Int(tempStep / magPow + 0.5)
        let stepSize = magMsd * Int(magPow)

        // build Y label array.
        // Lower and upper bounds calculations
        let lb = stepSize * Int(yMin/stepSize)
        let ub = stepSize * Int(ceil(CGFloat(yMax)/CGFloat(stepSize)))
        // Build array
        var val = lb
        while true {
            result.append(val)
            val += stepSize
            if val > ub { break }
        }
        return result
    }

}
Petr Syrov
fuente
Esto es excelente a menos que las entradas no sean enteros y sean números pequeños, por ejemplo, si yMin = 0.03 e yMax = 0.11.
Greg
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
mario
fuente
1

Para cualquiera que necesite esto en ES5 Javascript, ha estado luchando un poco, pero aquí está:

var min=52;
var max=173;
var actualHeight=500; // 500 pixels high graph

var tickCount =Math.round(actualHeight/100); 
// we want lines about every 100 pixels.

if(tickCount <3) tickCount =3; 
var range=Math.abs(max-min);
var unroundedTickSize = range/(tickCount-1);
var x = Math.ceil(Math.log10(unroundedTickSize)-1);
var pow10x = Math.pow(10, x);
var roundedTickRange = Math.ceil(unroundedTickSize / pow10x) * pow10x;
var min_rounded=roundedTickRange * Math.floor(min/roundedTickRange);
var max_rounded= roundedTickRange * Math.ceil(max/roundedTickRange);
var nr=tickCount;
var str="";
for(var x=min_rounded;x<=max_rounded;x+=roundedTickRange)
{
    str+=x+", ";
}
console.log("nice Y axis "+str);    

Basado en la excelente respuesta de Toon Krijtje.

Hjalmar Snoep
fuente
1

Esta solución se basa en un ejemplo de Java que encontré.

const niceScale = ( minPoint, maxPoint, maxTicks) => {
    const niceNum = ( localRange,  round) => {
        var exponent,fraction,niceFraction;
        exponent = Math.floor(Math.log10(localRange));
        fraction = localRange / Math.pow(10, exponent);
        if (round) {
            if (fraction < 1.5) niceFraction = 1;
            else if (fraction < 3) niceFraction = 2;
            else if (fraction < 7) niceFraction = 5;
            else niceFraction = 10;
        } else {
            if (fraction <= 1) niceFraction = 1;
            else if (fraction <= 2) niceFraction = 2;
            else if (fraction <= 5) niceFraction = 5;
            else niceFraction = 10;
        }
        return niceFraction * Math.pow(10, exponent);
    }
    const result = [];
    const range = niceNum(maxPoint - minPoint, false);
    const stepSize = niceNum(range / (maxTicks - 1), true);
    const lBound = Math.floor(minPoint / stepSize) * stepSize;
    const uBound = Math.ceil(maxPoint / stepSize) * stepSize;
    for(let i=lBound;i<=uBound;i+=stepSize) result.push(i);
    return result;
};
console.log(niceScale(15,234,6));
// > [0, 100, 200, 300]

pollos
fuente
0

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

rango de tick = 21,9. Esto debería ser 25.0

Para hacer esto algorítmicamente, ¿uno tendría que agregar lógica al algoritmo anterior para hacer que esta escala sea adecuada para números más grandes? Por ejemplo, con 10 ticks, si el rango es 3346, entonces el rango de ticks se evaluaría a 334.6 y redondeando a los 10 más cercanos daría 340 cuando 350 es probablemente más agradable.

¿Qué piensas?

theringostarrs
fuente
En el ejemplo de @ Gamecat, 334,6 => 0,3346, que debería ir a 0,4. Entonces, el rango de ticks sería 400, que es un número bastante bueno.
Bryan
0

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);
    }
}
Neil
fuente
0

Los algoritmos anteriores no tienen en cuenta el caso en el que el rango entre el valor mínimo y máximo es demasiado pequeño. ¿Y 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, tenemos que 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;
}
panorámicas
fuente