Выбор привлекательной линейной шкалы для оси Y графика - PullRequest
68 голосов
/ 29 ноября 2008

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

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

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

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

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

Ответы [ 12 ]

90 голосов
/ 29 ноября 2008

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

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

Давайте возьмем ваш пример:

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 ^ х.

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

Давайте рассмотрим тот же пример с 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 на один меньше, чем количество тиков.

19 голосов
/ 25 января 2012

Вот пример 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
)
8 голосов
/ 06 октября 2009

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

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 голосов
/ 29 ноября 2008

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

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

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

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 голосов
/ 02 апреля 2017

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

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

    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 с тем же размером тика. Но это мой уволенный, я оставляю это тебе.

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

3 голосов
/ 10 ноября 2012

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

Оригинальный ответ 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% времени .... что вполне может быть где-то моя ошибка, конечно!

1 голос
/ 01 июля 2012

это работает как шарм, если вы хотите 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
0 голосов
/ 04 июня 2019

Для тех, кому это нужно в 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.

0 голосов
/ 14 марта 2019

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

}
0 голосов
/ 30 сентября 2016

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