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

Comments