Выбор привлекательного линейного масштаба для оси Y графика

84

Я пишу небольшой код для отображения столбчатого (или линейного) графика в нашем программном обеспечении. Все идет хорошо. То, что меня озадачило, - это маркировка оси Y.

Звонящий может сказать мне, насколько точно они хотят пометить шкалу Y, но я, кажется, зациклился на том, что именно назвать им «привлекательным» способом. Я не могу описать "привлекательный", и, вероятно, вы тоже не можете, но мы узнаем это, когда видим это, верно?

Итак, если точки данных:

   15, 234, 140, 65, 90

И пользователь просит 10 меток по оси Y, немного поигрался с бумагой и карандашом:

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

Итак, там 10 (не считая 0), последнее выходит за пределы самого высокого значения (234 <250), и это «хорошее» приращение по 25 каждое. Если бы они попросили 8 меток, было бы неплохо прибавить 30:

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

Девять было бы непросто. Может быть, просто использовал 8 или 10 и назвал бы это достаточно близко, было бы нормально. А что делать, если часть баллов отрицательная?

Я вижу, что Excel прекрасно справляется с этой проблемой.

Кто-нибудь знает алгоритм общего назначения (даже грубая сила в порядке) для решения этой проблемы? Мне не нужно делать это быстро, но все должно хорошо выглядеть.

Клинтон Пирс
источник
1
Некоторая информация о том, как Excel выбирает максимальные и минимальные значения для своей оси Y, находится здесь: support.microsoft.com/kb/214075
Кристофер Орр,
Хорошая реализация: stackoverflow.com/a/16363437/829571
assylias

Ответы:

103

Давным-давно я написал модуль графа, который хорошо это покрыл. Копаясь в серой массе, получаем следующее:

  • Определите нижнюю и верхнюю границы данных. (Остерегайтесь особого случая, когда нижняя граница = верхняя граница!
  • Разделите диапазон на необходимое количество тиков.
  • Округлите диапазон тиков до подходящих значений.
  • Отрегулируйте соответственно нижнюю и верхнюю границу.

Возьмем ваш пример:

15, 234, 140, 65, 90 with 10 ticks
  1. нижняя граница = 15
  2. верхняя граница = 234
  3. диапазон = 234-15 = 219
  4. диапазон тика = 21,9. Это должно быть 25,0
  5. новая нижняя граница = 25 * раунд (15/25) = 0
  6. новая верхняя граница = 25 * раунд (1 + 235/25) = 250

Итак, диапазон = 0,25,50, ..., 225,250

Вы можете получить хороший диапазон тиков, выполнив следующие действия:

  1. разделите на 10 ^ x так, чтобы результат лежал в диапазоне от 0,1 до 1,0 (включая 0,1 без учета 1).
  2. переводим соответственно:
    • 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. умножить на 10 ^ x.

В этом случае 21,9 делится на 10 ^ 2 и получается 0,219. Это <= 0,25, поэтому теперь у нас 0,25. Умноженное на 10 ^ 2, получаем 25.

Давайте посмотрим на тот же пример с 8 отметками:

15, 234, 140, 65, 90 with 8 ticks
  1. нижняя граница = 15
  2. верхняя граница = 234
  3. диапазон = 234-15 = 219
  4. тиковый диапазон = 27,375
    1. Делим на 10 ^ 2, получаем 0,27375, получаем 0,3, что дает (умноженное на 10 ^ 2) 30.
  5. новая нижняя граница = 30 * раунд (15/30) = 0
  6. новая верхняя граница = 30 * раунд (1 + 235/30) = 240

Что дает результат, который вы запрашивали ;-).

------ Добавил KD ------

Вот код, который реализует этот алгоритм без использования таблиц поиска и т. Д.:

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;

Вообще говоря, количество тактов включает нижнюю галочку, поэтому фактические сегменты оси Y на единицу меньше количества тактов.

Мультяшный Крайте
источник
1
Это было примерно правильно. Шаг 3, мне пришлось уменьшить X на 1. Чтобы получить диапазон от 219 до 0,1-> 1, мне нужно разделить на 10 ^ 3 (1000), а не на 10 ^ 2 (100). В противном случае - на место.
Клинтон Пирс,
2
Вы ссылаетесь на деление на 10 ^ x и умножение на 10 ^ x. Следует отметить, что x можно найти следующим образом: 'double x = Math.Ceiling (Math.Log10 (tickRange));'
Брайан
1
Очень полезно. Хотя не понимал - «новая нижняя граница = 30 * раунд (15/30) = 0» (думаю, будет 30) и как вы получили 235 в «новой верхней границе = 30 * раунд (1 + 235/30) = 240 '235 нигде не упоминается, должно быть 234.
Mutant
4
Это отличный ответ. Очень признателен.
Джоэл Анаир
4
@JoelAnair Спасибо, ты только что сделал печальный день немного ярче.
Toon Krijthe
22

Вот пример PHP, который я использую. Эта функция возвращает массив красивых значений оси Y, которые включают переданные минимальные и максимальные значения Y. Конечно, эту процедуру также можно использовать для значений оси X.

Он позволяет вам «предложить», сколько тиков вы можете захотеть, но процедура вернет то, что выглядит хорошо. Я добавил несколько примеров данных и показал результаты для них.

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

Результат вывода из выборки данных

# ./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
)
Скотт Гатри
источник
мой босс будет доволен этим - тоже спасибо и от меня СПАСИБО !!
Стивен Хейзел
Отличный ответ! Я конвертировал его в Swift 4 stackoverflow.com/a/55151115/2670547
Петр Сыров
@Scott Guthrie: Это замечательно, если входные данные не являются целыми числами и представляют собой небольшие числа, например, если yMin = 0,03 и yMax = 0,11.
Грег
9

Попробуйте этот код. Я использовал его в нескольких сценариях построения графиков, и он хорошо работает. Это тоже довольно быстро.

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;
    }
}
Дрю Ноукс
источник
6

Похоже, вызывающий абонент не сообщает вам диапазоны, которые ему нужны.

Таким образом, вы можете изменять конечные точки до тех пор, пока они не будут точно делиться на количество ваших меток.

Определим «симпатичный». Я бы назвал хорошим, если бы ярлыки были отключены:

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

Найдите максимальные и минимальные значения вашего ряда данных. Назовем эти точки:

min_point and max_point.

Теперь все, что вам нужно сделать, это найти 3 значения:

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

которые соответствуют уравнению:

(end_label - start_label)/label_offset == label_count

Вероятно, существует множество решений, поэтому просто выберите одно. Бьюсь об заклад, в большинстве случаев вы можете установить

start_label to 0

так что просто попробуйте другое целое число

end_label

пока смещение не станет "хорошим"

Пиролитический
источник
3

Я все еще борюсь с этим :)

Исходный ответ Gamecat, кажется, работает большую часть времени, но попробуйте подключить, скажем, «3 тика» в качестве необходимого количества тиков (для тех же значений данных 15, 234, 140, 65, 90) .... кажется, дает диапазон тиков 73, который после деления на 10 ^ 2 дает 0,73, что соответствует 0,75, что дает «хороший» диапазон тиков 75.

Затем вычисляем верхнюю границу: 75 * раунд (1 + 234/75) = 300

и нижняя граница: 75 * раунд (15/75) = 0

Но очевидно, что если вы начнете с 0 и продвинетесь с шагом 75 до верхней границы 300, вы получите 0,75,150,225,300 .... что, несомненно, полезно, но это 4 такта (не включая 0), а не Требуется 3 галочки.

Просто расстраивает то, что он не работает в 100% случаев ... что, конечно, может быть связано с моей ошибкой!

Все еще размышляя
источник
Первоначально предполагалось, что проблема может быть связана с методом, предложенным Брайаном для получения x, но это, конечно, совершенно точно.
StillPondering
3

Ответ Toon Krijthe работает в большинстве случаев. Но иногда это приводит к избыточному количеству клещей. Он также не будет работать с отрицательными числами. Общий подход к проблеме приемлем, но есть способ лучше справиться с этим. Алгоритм, который вы хотите использовать, будет зависеть от того, что вы действительно хотите получить. Ниже я представляю вам свой код, который я использовал в своей библиотеке JS Ploting. Я тестировал его, и он всегда работает (надеюсь;)). Вот основные шаги:

  • получить глобальные экстремумы xMin и xMax (включая все графики, которые вы хотите распечатать в алгоритме)
  • рассчитать диапазон между xMin и xMax
  • рассчитайте порядок вашего диапазона
  • рассчитать размер тика, разделив диапазон на количество тиков минус один
  • это необязательно. Если вы хотите всегда печатать нулевые отметки, используйте размер галочки для расчета количества положительных и отрицательных отметок. Общее количество тиков будет их суммой + 1 (нулевой тик)
  • в этом нет необходимости, если у вас всегда напечатан ноль галочки. Рассчитайте нижнюю и верхнюю границу, но не забудьте центрировать график

Давайте начнем. Сначала основные расчеты

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

Я округляю минимальные и максимальные значения, чтобы быть на 100% уверенным, что мой график будет охватывать все данные. Также очень важно указать логарифм 10 диапазона, отрицательный он или нет, и вычесть 1 позже. В противном случае ваш алгоритм не будет работать для чисел меньше единицы.

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

Я использую «красивые галочки», чтобы избежать таких отметок, как 7, 13, 17 и т. Д. Метод, который я использую здесь, довольно прост. Также неплохо иметь zeroTick, когда это необходимо. Так сюжет выглядит намного профессиональнее. Вы найдете все методы в конце этого ответа.

Теперь вам нужно вычислить верхнюю и нижнюю границы. Это очень просто с нулевым тиком, но в другом случае потребуется немного больше усилий. Почему? Потому что мы хотим точно центрировать график в пределах верхней и нижней границы. Взгляни на мой код. Некоторые из переменных определены вне этой области, а некоторые из них являются свойствами объекта, в котором хранится весь представленный код.

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

И вот методы, о которых я упоминал ранее, которые вы можете написать самостоятельно, но вы также можете использовать мои

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

Есть только одна вещь, которой здесь нет. Это «красивые границы». Это нижние границы, которые представляют собой числа, похожие на числа в «красивых галочках». Например, лучше иметь нижнюю границу, начинающуюся с 5 с размером деления 5, чем иметь график, который начинается с 6 с таким же размером деления. Но это мой уволенный я оставляю это вам.

Надеюсь, поможет. Ура!

Артур
источник
2

Преобразовал этот ответ как 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
    }

}
Петр Сыров
источник
Это замечательно, если только входные данные не являются целыми числами и представляют собой небольшие числа, например, если yMin = 0,03 и yMax = 0,11.
Грег
1

это работает как шарм, если вы хотите 10 шагов + ноль

//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
Марио
источник
1

Для всех, кому это нужно в ES5 Javascript, немного поборолись, но вот оно:

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

На основе отличного ответа Toon Krijtje.

Яльмар Сноп
источник
1

Это решение основано на найденном мной примере Java .

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]

куры
источник
0

Спасибо за вопрос и ответ, очень полезно. Gamecat, мне интересно, как вы определяете, до какого диапазона тиков следует округлять.

диапазон тика = 21,9. Это должно быть 25,0

Чтобы сделать это алгоритмически, нужно было бы добавить логику к приведенному выше алгоритму, чтобы этот масштаб был удобен для больших чисел? Например, с 10 тиками, если диапазон равен 3346, тогда диапазон тиков будет равен 334,6, а округление до ближайших 10 даст 340, тогда как 350, вероятно, лучше.

Что вы думаете?

Theringostarrs
источник
В примере @ Gamecat 334.6 => 0.3346, что должно перейти на 0.4. Таким образом, диапазон тиков на самом деле будет 400, что является довольно приятным числом.
Брайан
0

На основе алгоритма @ Gamecat я создал следующий вспомогательный класс

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

Вышеупомянутые алгоритмы не учитывают случай, когда диапазон между минимальным и максимальным значением слишком мал. А что, если эти значения намного больше нуля? Затем у нас есть возможность начать ось Y со значением выше нуля. Кроме того, чтобы наша линия не находилась полностью в верхней или нижней части графика, мы должны дать ей немного «воздуха для дыхания».

Чтобы охватить эти случаи, я написал (на PHP) приведенный выше код:

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;
}
панорама
источник