Подтвердить что ты не робот

Алгоритм "приятных" интервалов сетки на графике

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

FYI, с вашими данными примера:

  • Maple: Min = 8, Max = 74, Labels = 10,20,.., 60,70, Ticks = 10,12,14,.. 70,72
  • MATLAB: Min = 10, Max = 80, Labels = 10,20,.., 60,80

Ответ 2

Я сделал это с помощью метода грубой силы. Сначала выясните максимальное количество меток, которые вы можете поместить в пространство. Разделите общий диапазон значений на количество тиков; это минимальный интервал галочки. Теперь вычислите пол логарифмической базы 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

Ответ 3

Есть две проблемы:

  • Определите порядок величины и
  • Раунд к чему-то удобному.

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

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

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

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

value_per_division = magnitude / subdivisions;

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

Ответ 4

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

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

Ответ 5

Вот еще одна реализация в 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;
};

Ответ 6

Я написал метод 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

Ответ 7

Взято из вышеприведенного, немного более полного класса Util в С#. Это также рассчитывает подходящий первый и последний тик.

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;

Ответ 8

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

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

Что касается выбора интервала тика, я бы предложил любое число формы 10 ^ x * i/n, где я < n и 0 < n < 10. Создайте этот список и отсортируйте его, и вы можете найти наибольшее число, меньшее, чем value_per_division (как в adam_liss), используя двоичный поиск.

Ответ 10

Используя много вдохновения из уже имеющихся здесь вариантов ответов, здесь моя реализация в C. Обратите внимание, что существует некоторая расширяемость, встроенная в массив ndex.

float findNiceDelta(float maxvalue, int count)
{
    float step = maxvalue/count,
         order = powf(10, floorf(log10(step))),
         delta = (int)(step/order + 0.5);

    static float ndex[] = {1, 1.5, 2, 2.5, 5, 10};
    static int ndexLenght = sizeof(ndex)/sizeof(float);
    for(int i = ndexLenght - 2; i > 0; --i)
        if(delta > ndex[i]) return ndex[i + 1] * order;
    return delta*order;
}

Ответ 11

В R используйте

tickSize <- function(range,minCount){
    logMaxTick <- log10(range/minCount)
    exponent <- floor(logMaxTick)
    mantissa <- 10^(logMaxTick-exponent)
    af <- c(1,2,5) # allowed factors
    mantissa <- af[findInterval(mantissa,af)]
    return(mantissa*10^exponent)
}

где аргумент диапазона - max-min домена.

Ответ 12

Вот функция javascript, которую я написал для округлых интервалов сетки (max-min)/gridLinesNumber до красивых значений. Он работает с любыми числами, см. gist с подробными кометами, чтобы узнать, как он работает и как его называть.

var ceilAbs = function(num, to, bias) {
  if (to == undefined) to = [-2, -5, -10]
  if (bias == undefined) bias = 0
  var numAbs = Math.abs(num) - bias
  var exp = Math.floor( Math.log10(numAbs) )

    if (typeof to == 'number') {
        return Math.sign(num) * to * Math.ceil(numAbs/to) + bias
    }

  var mults = to.filter(function(value) {return value > 0})
  to = to.filter(function(value) {return value < 0}).map(Math.abs)
  var m = Math.abs(numAbs) * Math.pow(10, -exp)
  var mRounded = Infinity

  for (var i=0; i<mults.length; i++) {
    var candidate = mults[i] * Math.ceil(m / mults[i])
    if (candidate < mRounded)
      mRounded = candidate
  }
  for (var i=0; i<to.length; i++) {
    if (to[i] >= m && to[i] < mRounded)
      mRounded = to[i]
  }
  return Math.sign(num) * mRounded * Math.pow(10, exp) + bias
}

Вызов ceilAbs(number, [0.5]) для разных номеров будет округлять такие числа:

301573431.1193228 -> 350000000
14127.786597236991 -> 15000
-63105746.17236853 -> -65000000
-718854.2201183736 -> -750000
-700660.340487957 -> -750000
0.055717507097870114 -> 0.06
0.0008068701205775142 -> 0.00085
-8.66660070605576 -> -9
-400.09256079792976 -> -450
0.0011740548815578223 -> 0.0015
-5.3003294346854085e-8 -> -6e-8
-0.00005815960629843176 -> -0.00006
-742465964.5184875 -> -750000000
-81289225.90985894 -> -85000000
0.000901771713513881 -> 0.00095
-652726598.5496342 -> -700000000
-0.6498901364393532 -> -0.65
0.9978325804695487 -> 1
5409.4078950583935 -> 5500
26906671.095639467 -> 30000000

Ознакомьтесь с fiddle, чтобы поэкспериментировать с кодом. Код в ответе, суть и скрипка немного разные. Я использую тот, который указан в ответе.

Ответ 13

Если вы пытаетесь получить весы, смотрящие прямо на диаграммах VB.NET, я использовал пример от Адама Лисса, но убедитесь, что когда вы задаете значения шкалы min и max, которые вы передаете им из переменной (не одно или два типа), в противном случае значения метки будут установлены как 8 знаков после запятой. Так, например, у меня было 1 диаграмма, где я устанавливал значение минимальной оси Y равным 0,0001, а максимальное значение оси Y - 0,002. Если я передам эти значения объекту диаграммы в качестве синглов, я получаю значения отметки 0.00048000001697801, 0.000860000036482233.... Если я передам эти значения объекту диаграммы в виде десятичных знаков, я получаю хорошие значения отметки 0.00048, 0.00086......

Ответ 14

В питоне:

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