Алгоритм «хороших» интервалов линий сетки на графике - PullRequest
59 голосов
/ 12 декабря 2008

Мне нужен достаточно умный алгоритм, чтобы придумать «красивые» линии сетки для графика (диаграммы).

Например, предположим гистограмму со значениями 10, 30, 72 и 60. Вы знаете:

Минимальное значение: 10 Максимальное значение: 72 Диапазон: 62

Первый вопрос: с чего начать? В этом случае 0 будет интуитивно понятным значением, но оно не будет поддерживаться другими наборами данных, поэтому я предполагаю:

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

Максимальное значение сетки должно быть «хорошим» значением выше максимального значения в диапазоне. В качестве альтернативы его можно указать (например, вы можете захотеть от 0 до 100, если вы показываете проценты, независимо от фактических значений).

Количество линий сетки (отметок) в диапазоне должно быть либо указано, либо число в данном диапазоне (например, 3-8), чтобы значения были «хорошими» (т. Е. Круглыми числами), и вы максимально используете область диаграммы. В нашем примере 80 было бы разумным максимумом, поскольку при этом использовалось бы 90% высоты диаграммы (72/80), тогда как 100 создавало бы больше потерянного пространства.

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

Ответы [ 14 ]

34 голосов
/ 12 декабря 2008

Я сделал это с помощью метода грубой силы. Сначала определите максимальное количество отметок, которые вы можете поместить в пространство. Разделите общий диапазон значений на количество тиков; это минимальный интервал для галочки. Теперь вычислите пол основания логарифма 10, чтобы получить величину тика, и разделите на это значение. Вы должны получить что-то в диапазоне от 1 до 10. Просто выберите округленное число, большее или равное значению, и умножьте его на логарифм, рассчитанный ранее. Это ваш последний интервал между тиками.

Пример на Python:

import math

def BestTick(largest, mostticks):
    minimum = largest / mostticks
    magnitude = 10 ** math.floor(math.log(minimum, 10))
    residual = minimum / magnitude
    if residual > 5:
        tick = 10 * magnitude
    elif residual > 2:
        tick = 5 * magnitude
    elif residual > 1:
        tick = 2 * magnitude
    else:
        tick = magnitude
    return tick

Редактировать: вы можете изменять выбор «хороших» интервалов. Один комментатор, похоже, недоволен предоставленными вариантами, потому что фактическое количество тиков может быть в 2,5 раза меньше максимального. Вот небольшая модификация, которая определяет таблицу для хороших интервалов. В этом примере я расширил выбор, чтобы число тиков не превышало 3/5 от максимального.

import bisect

def BestTick2(largest, mostticks):
    minimum = largest / mostticks
    magnitude = 10 ** math.floor(math.log(minimum, 10))
    residual = minimum / magnitude
    # this table must begin with 1 and end with 10
    table = [1, 1.5, 2, 3, 5, 7, 10]
    tick = table[bisect.bisect_right(table, residual)] if residual < 10 else 10
    return tick * magnitude
29 голосов
/ 12 декабря 2008

Есть 2 штуки к проблеме:

  1. Определите порядок величины и
  2. Округление до чего-нибудь удобного.

Вы можете обработать первую часть, используя логарифмы:

range = max - min;  
exponent = int(log(range));       // See comment below.
magnitude = pow(10, exponent);

Так, например, если ваш диапазон составляет от 50 до 1200, показатель степени равен 3, а величина равна 1000.

Затем разберитесь со второй частью, решив, сколько подразделений вы хотите в вашей сетке:

value_per_division = magnitude / subdivisions;

Это грубый расчет, поскольку показатель степени был усечен до целого числа. Возможно, вы захотите настроить вычисление экспоненты для лучшей обработки граничных условий, например, путем округления вместо взятия int(), если у вас слишком много подразделений.

14 голосов
/ 13 февраля 2009

Я использую следующий алгоритм. Это похоже на другие опубликованные здесь, но это первый пример в C #.

public static class AxisUtil
{
    public static float CalcStepSize(float range, float targetSteps)
    {
        // calculate an initial guess at step size
        var tempStep = range/targetSteps;

        // get the magnitude of the step size
        var mag = (float)Math.Floor(Math.Log10(tempStep));
        var magPow = (float)Math.Pow(10, mag);

        // calculate most significant digit of the new step size
        var magMsd = (int)(tempStep/magPow + 0.5);

        // promote the MSD to either 1, 2, or 5
        if (magMsd > 5)
            magMsd = 10;
        else if (magMsd > 2)
            magMsd = 5;
        else if (magMsd > 1)
            magMsd = 2;

        return magMsd*magPow;
    }
}
8 голосов
/ 12 декабря 2008

CPAN обеспечивает реализацию здесь (см. Ссылку на источник)

См. Также Алгоритм отметки для оси графика

К вашему сведению, с вашими примерами данных:

  • Клен: Мин = 8, Макс = 74, Ярлыки = 10,20, .., 60,70, Тики = 10,12,14, .. 70,72
  • MATLAB: Мин = 10, Макс = 80, Метки = 10,20, .., 60,80
5 голосов
/ 25 февраля 2013

Вот еще одна реализация в JavaScript:

var calcStepSize = function(range, targetSteps)
{
  // calculate an initial guess at step size
  var tempStep = range / targetSteps;

  // get the magnitude of the step size
  var mag = Math.floor(Math.log(tempStep) / Math.LN10);
  var magPow = Math.pow(10, mag);

  // calculate most significant digit of the new step size
  var magMsd = Math.round(tempStep / magPow + 0.5);

  // promote the MSD to either 1, 2, or 5
  if (magMsd > 5.0)
    magMsd = 10.0;
  else if (magMsd > 2.0)
    magMsd = 5.0;
  else if (magMsd > 1.0)
    magMsd = 2.0;

  return magMsd * magPow;
};
2 голосов
/ 01 октября 2014

Взято у Марка, чуть более полный класс Util в c #. Это также вычисляет подходящий первый и последний тик.

public  class AxisAssists
{
    public double Tick { get; private set; }

    public AxisAssists(double aTick)
    {
        Tick = aTick;
    }
    public AxisAssists(double range, int mostticks)
    {
        var minimum = range / mostticks;
        var magnitude = Math.Pow(10.0, (Math.Floor(Math.Log(minimum) / Math.Log(10))));
        var residual = minimum / magnitude;
        if (residual > 5)
        {
            Tick = 10 * magnitude;
        }
        else if (residual > 2)
        {
            Tick = 5 * magnitude;
        }
        else if (residual > 1)
        {
            Tick = 2 * magnitude;
        }
        else
        {
            Tick = magnitude;
        }
    }

    public double GetClosestTickBelow(double v)
    {
        return Tick* Math.Floor(v / Tick);
    }
    public double GetClosestTickAbove(double v)
    {
        return Tick * Math.Ceiling(v / Tick);
    }

}
With ability to create an instance ,but if you just want calculate and throw it away:   
double tickX = new AxisAssists(aMaxX - aMinX, 8).Tick;
2 голосов
/ 04 апреля 2014

Я написал метод Objective-C, который возвращает хороший масштаб оси и хорошие тики для заданных минимальных и максимальных значений вашего набора данных:

- (NSArray*)niceAxis:(double)minValue :(double)maxValue
{
    double min_ = 0, max_ = 0, min = minValue, max = maxValue, power = 0, factor = 0, tickWidth, minAxisValue = 0, maxAxisValue = 0;
    NSArray *factorArray = [NSArray arrayWithObjects:@"0.0f",@"1.2f",@"2.5f",@"5.0f",@"10.0f",nil];
    NSArray *scalarArray = [NSArray arrayWithObjects:@"0.2f",@"0.2f",@"0.5f",@"1.0f",@"2.0f",nil];

    // calculate x-axis nice scale and ticks
    // 1. min_
    if (min == 0) {
        min_ = 0;
    }
    else if (min > 0) {
        min_ = MAX(0, min-(max-min)/100);
    }
    else {
        min_ = min-(max-min)/100;
    }

    // 2. max_
    if (max == 0) {
        if (min == 0) {
            max_ = 1;
        }
        else {
            max_ = 0;
        }
    }
    else if (max < 0) {
        max_ = MIN(0, max+(max-min)/100);
    }
    else {
        max_ = max+(max-min)/100;
    }

    // 3. power
    power = log(max_ - min_) / log(10);

    // 4. factor
    factor = pow(10, power - floor(power));

    // 5. nice ticks
    for (NSInteger i = 0; factor > [[factorArray objectAtIndex:i]doubleValue] ; i++) {
        tickWidth = [[scalarArray objectAtIndex:i]doubleValue] * pow(10, floor(power));
    }

    // 6. min-axisValues
    minAxisValue = tickWidth * floor(min_/tickWidth);

    // 7. min-axisValues
    maxAxisValue = tickWidth * floor((max_/tickWidth)+1);

    // 8. create NSArray to return
    NSArray *niceAxisValues = [NSArray arrayWithObjects:[NSNumber numberWithDouble:minAxisValue], [NSNumber numberWithDouble:maxAxisValue],[NSNumber numberWithDouble:tickWidth], nil];

    return niceAxisValues;
}

Вы можете вызвать метод следующим образом:

NSArray *niceYAxisValues = [self niceAxis:-maxy :maxy];

и получите настройку оси:

double minYAxisValue = [[niceYAxisValues objectAtIndex:0]doubleValue];
double maxYAxisValue = [[niceYAxisValues objectAtIndex:1]doubleValue];
double ticksYAxis = [[niceYAxisValues objectAtIndex:2]doubleValue];

На всякий случай, если вы хотите ограничить количество тактов оси, сделайте следующее:

NSInteger maxNumberOfTicks = 9;
NSInteger numberOfTicks = valueXRange / ticksXAxis;
NSInteger newNumberOfTicks = floor(numberOfTicks / (1 + floor(numberOfTicks/(maxNumberOfTicks+0.5))));
double newTicksXAxis = ticksXAxis * (1 + floor(numberOfTicks/(maxNumberOfTicks+0.5)));

Первая часть кода основана на вычислении, которое я нашел здесь , чтобы вычислить хороший масштаб оси графика и отметки, похожие на графики Excel. Он отлично работает для всех видов наборов данных. Вот пример реализации iPhone:

enter image description here

1 голос
/ 03 мая 2013

Я являюсь автором " Алгоритма оптимального масштабирования по оси диаграммы ". Раньше он размещался на trollop.org, но я недавно переместил домены / движки блогов.

Пожалуйста, смотрите мой ответ на связанный вопрос .

1 голос
/ 12 декабря 2008

Другая идея состоит в том, чтобы диапазон оси был диапазоном значений, но ставьте метки в соответствующей позиции. То есть для 7-22 делайте:

[- - - | - - - - | - - - - | - - ]
       10        15        20

Что касается выбора интервала между тиками, я бы предложил любое число вида 10 ^ x * i / n, где i

0 голосов
/ 01 января 2018

В питоне:

steps = [numpy.round(x) for x in np.linspace(min, max, num=num_of_steps)]
...