Escolha de uma escala linear atraente para o eixo Y de um gráfico

84

Estou escrevendo um código para exibir um gráfico de barras (ou linhas) em nosso software. Tudo está indo bem. O que me deixa perplexo é rotular o eixo Y.

A pessoa que ligou pode me dizer o quão bem eles querem que a escala Y seja rotulada, mas eu pareço estar preso no que exatamente rotulá-los de uma forma "atraente". Não consigo descrever "atraente", e provavelmente você também não, mas sabemos disso quando vemos, certo?

Portanto, se os pontos de dados forem:

   15, 234, 140, 65, 90

E o usuário pede 10 rótulos no eixo Y, um pouco de trapaça com papel e lápis chega com:

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

Portanto, há 10 ali (sem incluir 0), o último se estende um pouco além do valor mais alto (234 <250) e é um incremento "bom" de 25 cada. Se eles pedissem 8 rótulos, um incremento de 30 teria ficado bem:

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

Nove teria sido complicado. Talvez apenas usar 8 ou 10 e chamá-lo de perto estaria tudo bem. E o que fazer quando alguns dos pontos são negativos?

Posso ver que o Excel resolve esse problema muito bem.

Alguém conhece um algoritmo de uso geral (até mesmo alguma força bruta é aceitável) para resolver isso? Não preciso fazer isso rapidamente, mas deve ficar bem.

Clinton Pierce
fonte
1
Há algumas informações sobre como o Excel escolhe os valores máximo e mínimo para seu eixo Y aqui: support.microsoft.com/kb/214075
Christopher Orr
Boa implementação: stackoverflow.com/a/16363437/829571
assylias

Respostas:

103

Há muito tempo, escrevi um módulo gráfico que cobriu isso muito bem. Cavar na massa cinzenta obtém o seguinte:

  • Determine o limite inferior e superior dos dados. (Cuidado com o caso especial em que limite inferior = limite superior!
  • Divida o intervalo pela quantidade necessária de carrapatos.
  • Arredonde a faixa de carrapatos em boas quantidades
  • Ajuste o limite inferior e superior de acordo.

Vamos dar o seu exemplo:

15, 234, 140, 65, 90 with 10 ticks
  1. limite inferior = 15
  2. limite superior = 234
  3. intervalo = 234-15 = 219
  4. faixa de escala = 21,9. Deve ser 25,0
  5. novo limite inferior = 25 * rodada (15/25) = 0
  6. novo limite superior = 25 * rodada (1 + 235/25) = 250

Portanto, o intervalo = 0,25,50, ..., 225,250

Você pode obter o bom intervalo de escala com as seguintes etapas:

  1. divida por 10 ^ x de modo que o resultado fique entre 0,1 e 1,0 (incluindo 0,1 excluindo 1).
  2. traduza de acordo:
    • 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. multiplique por 10 ^ x.

Nesse caso, 21,9 é dividido por 10 ^ 2 para obter 0,219. Isso é <= 0,25, então agora temos 0,25. Multiplicado por 10 ^ 2, dá 25.

Vamos dar uma olhada no mesmo exemplo com 8 ticks:

15, 234, 140, 65, 90 with 8 ticks
  1. limite inferior = 15
  2. limite superior = 234
  3. intervalo = 234-15 = 219
  4. faixa de escala = 27,375
    1. Divida por 10 ^ 2 para 0,27375, se traduz em 0,3, o que dá (multiplicado por 10 ^ 2) 30.
  5. novo limite inferior = 30 * rodada (15/30) = 0
  6. novo limite superior = 30 * rodada (1 + 235/30) = 240

Que dá o resultado que você solicitou ;-).

------ Adicionado por KD ------

Aqui está o código que alcança esse algoritmo sem usar tabelas de pesquisa, 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;

De modo geral, o número de marcações inclui a marcação inferior, portanto, os segmentos reais do eixo y são um a menos que o número de marcações.

Toon Krijthe
fonte
1
Isso estava quase certo. Etapa 3, tive que reduzir X por 1. Para obter um intervalo de 219 a .1-> 1, tenho que dividir por 10 ^ 3 (1000) e não 10 ^ 2 (100). Caso contrário, local.
Clinton Pierce
2
Você faz referência à divisão por 10 ^ x e à multiplicação por 10 ^ x. Deve-se notar que x pode ser encontrado da seguinte forma: 'double x = Math.Ceiling (Math.Log10 (tickRange));'
Bryan
1
Muito útil. Embora não tenha entendido - 'novo limite inferior = 30 * rodada (15/30) = 0' (virá 30, eu acho) e como você conseguiu 235 em 'novo limite superior = 30 * rodada (1 + 235/30) = 240 '235 não é mencionado em lugar nenhum, deveria ser 234.
Mutant
4
Esta é uma ótima resposta. Muito apreciado.
Joel Anair
4
@JoelAnair Obrigado, você acaba de tornar um dia triste um pouco mais brilhante.
Toon Krijthe
22

Aqui está um exemplo de PHP que estou usando. Esta função retorna uma matriz de valores bonitos do eixo Y que abrangem os valores Y mínimo e máximo passados. Claro, essa rotina também pode ser usada para valores do eixo X.

Ele permite que você "sugira" quantos tiques deseja, mas a rotina retornará o que parece bom. Eu adicionei alguns dados de amostra e mostrei os resultados para eles.

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

Saída de resultado de dados de amostra

# ./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
fonte
meu chefe vai ficar feliz com isso - voto positivo de mim também n OBRIGADO!
Stephen Hazel,
Ótima resposta! Eu converti para Swift 4 stackoverflow.com/a/55151115/2670547
Petr Syrov
@Scott Guthrie: Isso é ótimo, a menos que as entradas não sejam inteiras e sejam números pequenos, por exemplo, se yMin = 0,03 e yMax = 0,11.
Greg,
9

Experimente este código. Eu usei em alguns cenários de gráficos e funciona bem. É muito rápido também.

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;
    }
}
Drew Noakes
fonte
6

Parece que o chamador não informa os intervalos que deseja.

Portanto, você é livre para alterar os pontos finais até que seja bem divisível pela contagem de rótulos.

Vamos definir "legal". Eu consideraria bom se os rótulos estivessem desativados 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, ...

Encontre o máximo e o mínimo de sua série de dados. Vamos chamar esses pontos:

min_point and max_point.

Agora, tudo que você precisa fazer é 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 encaixam na equação:

(end_label - start_label)/label_offset == label_count

Provavelmente há muitas soluções, então escolha apenas uma. Na maioria das vezes, aposto que você pode definir

start_label to 0

então tente um inteiro diferente

end_label

até que o deslocamento seja "bom"

Pirolístico
fonte
3

Eu ainda estou lutando com isso :)

A resposta Gamecat original parece funcionar na maioria das vezes, mas tente conectar, digamos, "3 ticks" como o número de ticks necessários (para os mesmos valores de dados 15, 234, 140, 65, 90) .... parece fornecer uma faixa de escala de 73, que após a divisão por 10 ^ 2 resulta em 0,73, que mapeia para 0,75, o que dá uma faixa de escala 'legal' de 75.

Em seguida, calculando o limite superior: 75 * redondo (1 + 234/75) = 300

e o limite inferior: 75 * rodada (15/75) = 0

Mas claramente se você começar em 0 e prosseguir nas etapas de 75 até o limite superior de 300, você terminará com 0,75,150,225,300 .... o que é sem dúvida útil, mas são 4 ticks (sem incluir 0), não o 3 carrapatos necessários.

Apenas frustrante que não funciona 100% do tempo .... o que pode muito bem ser devido ao meu erro em algum lugar, é claro!

StillPondering
fonte
Originalmente pensei que o problema pudesse ser algo a ver com o método sugerido de Bryan para derivar x, mas isso é perfeitamente correto.
StillPondering
3

A resposta de Toon Krijthe funciona na maioria das vezes. Mas às vezes ele produzirá um número excessivo de carrapatos. Também não funcionará com números negativos. A abordagem geral para o problema está ok, mas há uma maneira melhor de lidar com isso. O algoritmo que você deseja usar dependerá do que você realmente deseja obter. Abaixo estou apresentando meu código que usei em minha biblioteca JS Ploting. Eu testei e sempre funciona (espero;)). Aqui estão as etapas principais:

  • obter extremas globais xMin e xMax (inlucde todos os gráficos que você deseja imprimir no algoritmo)
  • calcular o intervalo entre xMin e xMax
  • calcule a ordem de magnitude do seu alcance
  • calcule o tamanho do tick dividindo o intervalo pelo número de ticks menos um
  • este é opcional. Se você deseja que o tique zero seja sempre impresso, use o tamanho do tique para calcular o número de tiques positivos e negativos. O número total de ticks será a soma + 1 (o tick zero)
  • este não é necessário se você tiver zero tick sempre impresso. Calcule os limites inferior e superior, mas lembre-se de centralizar o gráfico

Vamos começar. Primeiro os 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);

Arredondei os valores mínimo e máximo para ter 100% de certeza de que meu gráfico cobrirá todos os dados. Também é muito importante colocar o log10 de base na faixa se é negativo ou não e subtrair 1 depois. Caso contrário, seu algoritmo não funcionará para números menores que um.

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

Eu uso "carrapatos bonitos" para evitar carrapatos como 7, 13, 17, etc. O método que uso aqui é muito simples. Também é bom ter zeroTick quando necessário. O enredo parece muito mais profissional dessa forma. Você encontrará todos os métodos no final desta resposta.

Agora você precisa calcular os limites superior e inferior. Isso é muito fácil com zero tick, mas requer um pouco mais de esforço em outros casos. Por quê? Porque queremos centralizar o gráfico dentro dos limites superior e inferior de maneira adequada. Dê uma olhada no meu código. Algumas das variáveis ​​são definidas fora deste escopo e algumas delas são propriedades de um objeto no qual todo o código apresentado é mantido.

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

E aqui estão os métodos que mencionei antes que você pode escrever sozinho, mas também pode usar o meu

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

Há apenas mais uma coisa que não está incluída aqui. Este é o "limite bonito". Estes são os limites inferiores que são números semelhantes aos números em "marcas de boa aparência". Por exemplo, é melhor ter o limite inferior começando em 5 com tamanho de escala 5 do que ter um gráfico que começa em 6 com o mesmo tamanho de escala. Mas esta minha despedida deixo isso para você.

Espero que ajude. Felicidades!

Arthur
fonte
2

Esta resposta foi convertida em 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
fonte
Isso é ótimo, a menos que as entradas não sejam inteiras e sejam números pequenos, por exemplo, se yMin = 0,03 e yMax = 0,11.
Greg,
1

isso funciona perfeitamente, se você quiser 10 passos + zero

//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
fonte
1

Para quem precisa disso no Javascript ES5, tenho lutado um pouco, mas aqui 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);    

Com base na excelente resposta de Toon Krijtje.

Hjalmar Snoep
fonte
1

Esta solução é baseada em um exemplo Java que encontrei.

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]

galinhas
fonte
0

Obrigado pela pergunta e resposta, muito útil. Gamecat, estou querendo saber como você está determinando para que o intervalo de ticks deve ser arredondado.

faixa de escala = 21,9. Deve ser 25,0

Para fazer isso algoritmicamente, seria necessário adicionar lógica ao algoritmo acima para fazer essa escala bem para números maiores? Por exemplo, com 10 tiques, se o intervalo for 3346, então o intervalo de tiques seria avaliado como 334,6 e o ​​arredondamento para os 10 mais próximos resultaria em 340 quando 350 é provavelmente mais agradável.

O que você acha?

theringostarrs
fonte
No exemplo de @ Gamecat, 334,6 => 0,3346, que deve ir para 0,4. Portanto, o intervalo de escala seria, na verdade, 400, que é um número muito bom.
Bryan
0

Com base no algoritmo de @ Gamecat, produzi a seguinte classe auxiliar

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
fonte
0

Os algoritmos acima não levam em consideração o caso em que o intervalo entre o valor mínimo e máximo é muito pequeno. E se esses valores forem muito maiores que zero? Então, temos a possibilidade de iniciar o eixo y com um valor maior que zero. Além disso, para evitar que nossa linha fique inteiramente na parte superior ou inferior do gráfico, temos que dar a ela um pouco de "ar para respirar".

Para cobrir esses casos, escrevi (em PHP) o código acima:

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