Алгоритм "приятных" интервалов сетки на графике
Мне нужен разумно умный алгоритм, чтобы придумать "хорошие" сетки для графика (диаграммы).
Например, предположим, что гистограмма со значениями 10, 30, 72 и 60. Знаете:
Минимальное значение: 10
Максимальное значение: 72
Диапазон: 62
Первый вопрос: с чего вы начинаете? В этом случае 0 будет интуитивным значением, но это не будет задерживаться на других наборах данных, поэтому я предполагаю:
Значение минимальной сетки должно быть либо 0, либо "хорошим" значением ниже минимального значения данных в диапазоне. В качестве альтернативы его можно указать.
Значение максимальной сетки должно быть "хорошим" значением выше максимального значения в диапазоне. В качестве альтернативы, его можно указать (например, вам может понадобиться от 0 до 100, если вы показываете проценты, независимо от фактических значений).
Количество линий сетки (тиков) в диапазоне должно быть либо задано, либо число в заданном диапазоне (например, 3-8), так что значения являются "хорошими" (т.е. круглыми числами), и вы максимизируете использование области диаграммы. В нашем примере 80 будет разумным макс, так как он будет использовать 90% высоты диаграммы (72/80), тогда как 100 создадут больше потерянного пространства.
Кто-нибудь знает о хорошем алгоритме для этого? Язык не имеет значения, поскольку я его реализую в том, что мне нужно.
Ответы
Ответ 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), используя двоичный поиск.
Ответ 9
Я автор алгоритма для оптимального масштабирования на оси диаграммы". Раньше он был размещен на trollop.org, но недавно я перешел на серверы доменов/блогов.
Обратите внимание на мой ответ на соответствующий вопрос.
Ответ 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)]