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.
Respostas:
Há muito tempo, escrevi um módulo gráfico que cobriu isso muito bem. Cavar na massa cinzenta obtém o seguinte:
Vamos dar o seu exemplo:
Portanto, o intervalo = 0,25,50, ..., 225,250
Você pode obter o bom intervalo de escala com as seguintes etapas:
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:
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 ...:
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.
fonte
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
fonte
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; } }
fonte
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:
Encontre o máximo e o mínimo de sua série de dados. Vamos chamar esses pontos:
Agora, tudo que você precisa fazer é encontrar 3 valores:
que se encaixam na equação:
Provavelmente há muitas soluções, então escolha apenas uma. Na maioria das vezes, aposto que você pode definir
então tente um inteiro diferente
até que o deslocamento seja "bom"
fonte
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!
fonte
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:
Vamos começar. Primeiro os cálculos básicos
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.
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.
E aqui estão os métodos que mencionei antes que você pode escrever sozinho, mas também pode usar o meu
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!
fonte
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 } }
fonte
isso funciona perfeitamente, se você quiser 10 passos + zero
fonte
Para quem precisa disso no Javascript ES5, tenho lutado um pouco, mas aqui está:
Com base na excelente resposta de Toon Krijtje.
fonte
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]
fonte
Obrigado pela pergunta e resposta, muito útil. Gamecat, estou querendo saber como você está determinando para que o intervalo de ticks deve ser arredondado.
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?
fonte
Com base no algoritmo de @ Gamecat, produzi a seguinte classe auxiliar
fonte
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:
fonte