Алгоритм для" хороших " интервалов линий сетки на графике



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



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



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



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



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



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



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



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

642   14  

14 ответов:

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

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

FYI, с вашими данными по образца:

  • Maple: Min=8, Max=74, Labels=10,20,..,60,70, клещей=10,12,14,..70,72
  • MATLAB: Min=10, Max=80, Labels=10,20,,..,60,80

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

есть 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() Если вы в конечном итоге с слишком много подразделений.

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

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

var ln10 = Math.log(10);
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) / 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;
};

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

взятый из Mark выше, немного более полный класс 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;

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

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

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

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

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

используя много вдохновения от ответов, уже доступных здесь, вот моя реализация в 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;
}

в 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 домена.

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

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

Проверьте скрипка поэкспериментировать с кодом. Код в ответе, суть и скрипка немного отличается я использую один дано в ответ.

Если вы пытаетесь получить весы глядя прямо на VB.NET диаграммы, затем я использовал пример из Adam Liss, но убедитесь, что при установке значений шкалы min и max вы передаете их из переменной типа decimal (а не типа single или double), иначе значения делений в конечном итоге будут установлены как 8 десятичных знаков. Так, например, у меня была 1 диаграмма, где я установил минимальное значение оси Y в 0.0001 и максимальное значение оси Y в 0.002. Если я передаю эти значения в объект диаграммы как синглы я получаю значения галочек 0.00048000001697801, 0.000860000036482233 .... В то время как если я передаю эти значения в объект диаграммы в виде десятичных знаков, я получаю хорошие значения отметки 0.00048, 0.00086 ......

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

Comments

    Ничего не найдено.