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

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

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

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

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

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

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

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

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

12.12.2008 01:54:19
Я не могу разобрать часть Например. Можете ли вы привести более четкий пример?
S.Lott 12.12.2008 02:00:31
Как насчет хороших круглых чисел, таких как 2,4,8,16,32,64 ....
Kibbee 12.12.2008 02:03:58
@ S.Lott, например, у вас есть набор данных {10, 30, 72, 60}. Значения X (возможно, метки) расположены вдоль оси X графика; столбики поднимаются по оси Y на шкале до соответствующего значения в наборе данных.
strager 12.12.2008 02:20:25
@strager: спасибо, но это была единственная часть, которую я мог разобрать в оригинальном сообщении. Пересмотренный намного, намного яснее.
S.Lott 12.12.2008 02:23:09
14 ОТВЕТОВ
РЕШЕНИЕ

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

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

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

  • Клен: Мин = 8, Макс = 74, Метки = 10,20, .., 60,70, Тики = 10,12,14, .. 70,72
  • MATLAB: Мин. = 10, Макс. = 80, Метки = 10,20 ,, .., 60,80
11
23.05.2017 12:26:26

Я сделал это с помощью метода грубой силы. Сначала определите максимальное количество отметок, которые вы можете поместить в пространство. Разделите общий диапазон значений на количество тиков; это минимальный интервал для галочки. Теперь вычислите пол основания логарифма 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
34
1.08.2016 21:25:53
Время - это все: вы нажали «Опубликовать», а я все еще печатал примеры! Хорошее объяснение!
Adam Liss 12.12.2008 02:11:04
Но вы все еще получаете больше голосов, чем я. Как вы говорите, время это все, и вы были первыми!
Mark Ransom 12.12.2008 02:52:19
Вот вам голос за умное использование остатка. :-)
Adam Liss 12.12.2008 03:08:41
@MarkRansom У меня сейчас проблема, которая может быть решена с помощью этого ответа (см .: stackoverflow.com/questions/28987264 ), но я не уверен, как это сделать figure out the maximum number of tick marks you can fit into the space. Есть указатели? (Я знаю, что это было давно, но если бы вы могли дать более подробную информацию о вашем ответе, это было бы здорово. Спасибо!)
Gabriel 11.03.2015 20:25:27
@ Габриэль, извини, я ничего не знаю о matplotlib. Для общего вопроса, однако, вам нужно знать, сколько места занято одной меткой (с пространством, добавленным для заполнения) и сколько занято всей осью; разделите одно на другое, чтобы получить максимальное количество тиков.
Mark Ransom 11.03.2015 20:38:42

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

30
12.12.2008 02:09:48
Спасибо, помог мне решить проблему с логарифмическим масштабированием экономических показателей.
Richard Clayton 14.03.2012 04:01:17
Примечание для себя: «показатель степени» с использованием javascript с использованием base10 см. В принятом ответе по
mg1075 28.04.2012 18:35:26

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

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

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

1
12.12.2008 02:22:42

Я использую следующий алгоритм. Это похоже на другие опубликованные здесь, но это первый пример в 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;
    }
}
16
19.05.2017 20:03:47
На этой веб-странице показано, как также установить минимальное и максимальное значения для оси: trollop.org/2011/03/15/…
Curt 6.02.2012 19:14:00
@ Курт Спасибо! Эта ссылка сейчас мертва, но я нашел страницу здесь: esurient-systems.ca/2011/03/…
kol 30.09.2013 07:09:51
@ обе ссылки выше теперь мертвы. Я полагаю, что та же самая имплементация c # есть в erison.blogspot.com/2011/07/…
Bryant Miano 5.12.2015 04:58:14

Вот еще одна реализация в 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
27.05.2019 09:56:40
Просто именно то, что нужно. Хорошая работа.
ChiMo 29.10.2015 05:40:33
Этот метод требует изменения максимального количества броска в некоторых случаях.
puppeteer701 29.10.2015 16:56:56
Крошечный обман. Math.LN10? Известная константа кажется немного лучше, чем вызов функции!
Auspex 24.05.2019 14:23:05
хммм, предполагая, что min = 112, max = 5847 (диапазон = 5735), targetSteps = 8: calcStepSize (5735, 8) = 1000, но это слишком высоко, это значит иметь, например, 8 шагов, начиная с 0 и заканчивая до 8000
user2010955 1.07.2019 11:19:19
@ user2010955 какой выход вы ожидаете в таком случае?
Drew Noakes 2.07.2019 01:38:33

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

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

2
23.05.2017 12:34:38

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

В 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)
}

где аргумент диапазона - это максимальный минус домена.

0
4.10.2013 14:04:37

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

введите описание изображения здесь

2
11.04.2014 05:45:19
Что такое doubleValue в коде, я пытаюсь преобразовать его в JavaScript?
puppeteer701 2.11.2015 14:19:01
@ MatjažJurečič, это необходимо в цели C, чтобы считать значение как двойное. Я не уверен, но это, скорее всего, связано только с целью.
JFS 2.11.2015 19:22:48

Взято у Марка, чуть более полный класс 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);
    }
}

С возможностью создания экземпляра, но если вы просто хотите рассчитать и выбросить его:

    double tickX = new AxisAssists(aMaxX - aMinX, 8).Tick;
3
15.01.2020 04:07:55

Вот функция 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

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

0
7.02.2016 13:22:26

Если вы хотите, чтобы шкалы выглядели правильно на диаграммах VB.NET, то я использовал пример Адама Лисса, но убедитесь, что при установке значений шкалы min и max вы передаете их из переменной типа decimal (не типа single или double) в противном случае значения отметки в конечном итоге устанавливаются равными 8 десятичным знакам. Например, у меня был 1 график, где я установил минимальное значение оси Y на 0,0001, а максимальное значение оси Y на 0,002. Если я передаю эти значения объекту диаграммы в виде синглов, я получаю значения отметок 0,00048000001697801, 0,000860000036482233 .... Тогда как, если я передаю эти значения объекту диаграммы в виде десятичных дробей, я получаю хорошие значения отметок 0,00048, 0,00086 .... ..

0
8.04.2016 12:44:37

В питоне:

steps = [numpy.round(x) for x in np.linspace(min, max, num=num_of_steps)]
0
5.05.2019 20:48:55