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

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

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

Здесь есть некоторая информация о том, как Excel выбирает максимальные и минимальные значения для своей оси Y: support.microsoft.com/kb/214075

Christopher Orr 28.09.2011 21:30

Хорошая реализация: stackoverflow.com/a/16363437/829571

assylias 14.04.2016 18:47
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
85
2
43 110
13
Перейти к ответу Данный вопрос помечен как решенный

Ответы 13

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

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

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

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

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

Ответ принят как подходящий

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

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

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

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 на единицу меньше количества тактов.

Это было примерно правильно. Шаг 3, мне пришлось уменьшить X на 1. Чтобы получить диапазон от 219 до 0,1-> 1, мне нужно разделить на 10 ^ 3 (1000), а не на 10 ^ 2 (100). В противном случае - на место.

Clinton Pierce 01.12.2008 17:50

Вы ссылаетесь на деление на 10 ^ x и умножение на 10 ^ x. Следует отметить, что x можно найти так: 'double x = Math.Ceiling (Math.Log10 (tickRange));'

Bryan 16.12.2011 02:58

Очень полезно. Хотя не понимал - «новая нижняя граница = 30 * раунд (15/30) = 0» (я думаю, будет 30) и как вы получили 235 в «новой верхней границе = 30 * раунд (1 + 235/30) = 240 '235 нигде не упоминается, должно быть 234.

Mutant 20.12.2013 22:18

Это отличный ответ. Очень признателен.

Joel Anair 20.02.2014 00:09

@JoelAnair Спасибо, ты только что сделал печальный день немного ярче.

Toon Krijthe 20.02.2014 00:13

Фантастический ответ и алгоритм - один из самых совершенных ответов, которые я видел на SO.

Tom 10.06.2015 00:11

это действительно сильно меняет максимальный диапазон, и когда диапазон равен 140000000, вы получаете тот же округленныйTickRange, когда tickCount = [8,1 0, 11, 12, 13, 14].

puppeteer701 29.10.2015 20:03

он не работает правильно для огромных чисел, число слишком велико, и поэтому данные диаграммы выглядят очень маленькими

puppeteer701 30.10.2015 12:24

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

tick range = 21.9. This should be 25.0

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

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

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

Bryan 16.12.2011 02:54

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

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

Вот пример 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
)

мой босс будет доволен этим - тоже спасибо и от меня СПАСИБО !!

Stephen Hazel 25.03.2015 19:37

Отличный ответ! Конвертирую в Swift 4stackoverflow.com/a/55151115/2670547

Petr Syrov 14.03.2019 00:01

@Scott Guthrie: Это замечательно, если входные данные не являются целыми числами и представляют собой небольшие числа, например, если yMin = 0,03 и yMax = 0,11.

Greg 19.09.2020 18:51

это работает как шарм, если вы хотите 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

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

Исходный ответ 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 11.11.2012 13:29

На основе алгоритма @ 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);
    }
}

Вышеупомянутые алгоритмы не учитывают случай, когда диапазон между минимальным и максимальным значением слишком мал. А что, если эти значения намного больше нуля? Затем у нас есть возможность начать ось 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;
}

Ответ Мультяшный Крайте работает большую часть времени. Но иногда это приводит к избыточному количеству клещей. Он также не будет работать с отрицательными числами. Общий подход к проблеме приемлем, но есть способ лучше справиться с этим. Алгоритм, который вы хотите использовать, будет зависеть от того, что вы действительно хотите получить. Ниже я представляю вам свой код, который я использовал в своей библиотеке 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%, что мой график будет охватывать все данные. Также очень важно определить значение log10 диапазона, отрицательное или отрицательное, и вычесть 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 с таким же размером деления. Но это мое увольнение, я оставляю это вам.

Надеюсь, это поможет. Ваше здоровье!

Преобразовал этот отвечать в 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.

Greg 19.09.2020 18:51

Для тех, кому это нужно в 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.info("nice Y axis "+str);    

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

Это решение основано на найденном мной Пример 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.info(niceScale(15,234,6));
// > [0, 100, 200, 300]

Другие вопросы по теме