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

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

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

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

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

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

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

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

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

14 ответов


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

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

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

  • Клен: Мин=8, Макс=74, Ярлыки=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

Edit:вы можете изменить выбор" хороших " интервалов. Один комментатор, похоже, недоволен предоставленными выборками, потому что фактическое количество тиков может быть в 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


взято из отметки выше, немного более полный класс 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 график, где я установил значение min Y Axis в 0.0001 и максимальное значение Y Axis в 0.002. Если я передам эти значения объекту chart как синглы я получаю значения галочки 0.00048000001697801, 0.000860000036482233 .... В то время как если я передаю эти значения объекту диаграммы в виде десятичных знаков, я получаю хорошие значения отметки 0.00048, 0.00086 ......


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