Я пишу небольшой код для отображения столбчатого (или линейного) графика в нашем программном обеспечении. Все идет хорошо. То, что меня озадачило, - это маркировка оси Y.
Звонящий может сказать мне, насколько точно они хотят пометить шкалу Y, но я, кажется, зациклился на том, что именно назвать им «привлекательным» способом. Я не могу описать "привлекательный", и, вероятно, вы тоже не можете, но мы узнаем это, когда видим, верно?
Итак, если точки данных:
15, 234, 140, 65, 90
И пользователь просит 10 меток по оси Y, немного поигрался с бумагой и карандашом:
0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250
Итак, там 10 (не считая 0), последнее выходит за пределы самого высокого значения (234 <250), и это «приятное» приращение по 25 каждое. Если бы они попросили 8 меток, было бы неплохо приращение 30:
0, 30, 60, 90, 120, 150, 180, 210, 240
Девять было бы непросто. Может быть, просто использовал 8 или 10 и назвал бы это достаточно близко, было бы нормально. А что делать, если часть баллов отрицательная?
Я вижу, что Excel отлично справляется с этой проблемой.
Кто-нибудь знает алгоритм общего назначения (даже грубая сила в порядке) для решения этой проблемы? Мне не нужно делать это быстро, но все должно хорошо выглядеть.
Хорошая реализация: stackoverflow.com/a/16363437/829571





Звучит так, будто вызывающий абонент не сообщает вам диапазоны, которые ему нужны.
Таким образом, вы можете изменять конечные точки, пока не получите число, кратное количеству ваших меток.
Определим «симпатичный». Я бы назвал хорошим, если бы ярлыки были отключены:
1. 2^n, for some integer n. eg. ..., .25, .5, 1, 2, 4, 8, 16, ...
2. 10^n, for some integer n. eg. ..., .01, .1, 1, 10, 100
3. n/5 == 0, for some positive integer n, eg, 5, 10, 15, 20, 25, ...
4. n/2 == 0, for some positive integer n, eg, 2, 4, 6, 8, 10, 12, 14, ...
Найдите максимальные и минимальные значения вашего ряда данных. Назовем эти точки:
min_point and max_point.
Теперь все, что вам нужно сделать, это найти 3 значения:
- start_label, where start_label < min_point and start_label is an integer
- end_label, where end_label > max_point and end_label is an integer
- label_offset, where label_offset is "nice"
которые соответствуют уравнению:
(end_label - start_label)/label_offset == label_count
Вероятно, существует множество решений, поэтому просто выберите одно. Бьюсь об заклад, в большинстве случаев вы можете установить
start_label to 0
так что просто попробуйте другое целое число
end_label
пока смещение не станет "хорошим"
Давным-давно я написал модуль графа, который хорошо это покрыл. Копаясь в серой массе, получаем следующее:
Возьмем ваш пример:
15, 234, 140, 65, 90 with 10 ticks
Итак, диапазон = 0,25,50, ..., 225,250
Вы можете получить хороший диапазон тиков, выполнив следующие шаги:
В этом случае 21,9 делится на 10 ^ 2 и получается 0,219. Это <= 0,25, поэтому теперь у нас 0,25. Умноженное на 10 ^ 2, получаем 25.
Давайте посмотрим на тот же пример с 8 отметками:
15, 234, 140, 65, 90 with 8 ticks
Что дает результат, который вы запрашивали ;-).
------ Добавил KD ------
Вот код, который реализует этот алгоритм без использования таблиц поиска и т. д.:
double range = ...;
int tickCount = ...;
double unroundedTickSize = range/(tickCount-1);
double x = Math.ceil(Math.log10(unroundedTickSize)-1);
double pow10x = Math.pow(10, x);
double roundedTickRange = Math.ceil(unroundedTickSize / pow10x) * pow10x;
return roundedTickRange;
Вообще говоря, количество тактов включает нижний такт, поэтому фактические сегменты оси Y на единицу меньше количества тактов.
Это было примерно правильно. Шаг 3, мне пришлось уменьшить X на 1. Чтобы получить диапазон от 219 до 0,1-> 1, мне нужно разделить на 10 ^ 3 (1000), а не на 10 ^ 2 (100). В противном случае - на место.
Вы ссылаетесь на деление на 10 ^ x и умножение на 10 ^ x. Следует отметить, что x можно найти так: 'double x = Math.Ceiling (Math.Log10 (tickRange));'
Очень полезно. Хотя не понимал - «новая нижняя граница = 30 * раунд (15/30) = 0» (я думаю, будет 30) и как вы получили 235 в «новой верхней границе = 30 * раунд (1 + 235/30) = 240 '235 нигде не упоминается, должно быть 234.
Это отличный ответ. Очень признателен.
@JoelAnair Спасибо, ты только что сделал печальный день немного ярче.
Фантастический ответ и алгоритм - один из самых совершенных ответов, которые я видел на SO.
это действительно сильно меняет максимальный диапазон, и когда диапазон равен 140000000, вы получаете тот же округленныйTickRange, когда tickCount = [8,1 0, 11, 12, 13, 14].
он не работает правильно для огромных чисел, число слишком велико, и поэтому данные диаграммы выглядят очень маленькими
Спасибо за вопрос и ответ, очень полезно. Gamecat, мне интересно, как вы определяете, до какой величины следует округлять диапазон тиков.
tick range = 21.9. This should be 25.0
Чтобы сделать это алгоритмически, нужно было бы добавить логику к приведенному выше алгоритму, чтобы сделать этот масштаб подходящим для больших чисел? Например, при 10 тиках, если диапазон равен 3346, тогда диапазон тиков будет равен 334,6, а округление до ближайших 10 даст 340, тогда как 350, вероятно, лучше.
Что вы думаете?
В примере @ Gamecat 334.6 => 0.3346, что должно перейти на 0.4. Таким образом, диапазон тиков на самом деле будет 400, что является довольно приятным числом.
Попробуйте этот код. Я использовал его в нескольких сценариях построения графиков, и он хорошо работает. Это тоже довольно быстро.
public static class AxisUtil
{
public static float CalculateStepSize(float range, float targetSteps)
{
// calculate an initial guess at step size
float tempStep = range/targetSteps;
// get the magnitude of the step size
float mag = (float)Math.Floor(Math.Log10(tempStep));
float magPow = (float)Math.Pow(10, mag);
// calculate most significant digit of the new step size
float magMsd = (int)(tempStep/magPow + 0.5);
// promote the MSD to either 1, 2, or 5
if (magMsd > 5.0)
magMsd = 10.0f;
else if (magMsd > 2.0)
magMsd = 5.0f;
else if (magMsd > 1.0)
magMsd = 2.0f;
return magMsd*magPow;
}
}
Вот пример PHP, который я использую. Эта функция возвращает массив красивых значений оси Y, которые включают переданные минимальные и максимальные значения Y. Конечно, эту процедуру также можно использовать для значений оси X.
Это позволяет вам «предложить», сколько тиков вы можете захотеть, но процедура вернет что хорошо выглядит. Я добавил несколько примеров данных и показал результаты для них.
#!/usr/bin/php -q
<?php
function makeYaxis($yMin, $yMax, $ticks = 10)
{
// This routine creates the Y axis values for a graph.
//
// Calculate Min amd Max graphical labels and graph
// increments. The number of ticks defaults to
// 10 which is the SUGGESTED value. Any tick value
// entered is used as a suggested value which is
// adjusted to be a 'pretty' value.
//
// Output will be an array of the Y axis values that
// encompass the Y values.
$result = array();
// If yMin and yMax are identical, then
// adjust the yMin and yMax values to actually
// make a graph. Also avoids division by zero errors.
if ($yMin == $yMax)
{
$yMin = $yMin - 10; // some small value
$yMax = $yMax + 10; // some small value
}
// Determine Range
$range = $yMax - $yMin;
// Adjust ticks if needed
if ($ticks < 2)
$ticks = 2;
else if ($ticks > 2)
$ticks -= 2;
// Get raw step value
$tempStep = $range/$ticks;
// Calculate pretty step value
$mag = floor(log10($tempStep));
$magPow = pow(10,$mag);
$magMsd = (int)($tempStep/$magPow + 0.5);
$stepSize = $magMsd*$magPow;
// build Y label array.
// Lower and upper bounds calculations
$lb = $stepSize * floor($yMin/$stepSize);
$ub = $stepSize * ceil(($yMax/$stepSize));
// Build array
$val = $lb;
while(1)
{
$result[] = $val;
$val += $stepSize;
if ($val > $ub)
break;
}
return $result;
}
// Create some sample data for demonstration purposes
$yMin = 60;
$yMax = 330;
$scale = makeYaxis($yMin, $yMax);
print_r($scale);
$scale = makeYaxis($yMin, $yMax,5);
print_r($scale);
$yMin = 60847326;
$yMax = 73425330;
$scale = makeYaxis($yMin, $yMax);
print_r($scale);
?>
Результат вывода из выборки данных
# ./test1.php
Array
(
[0] => 60
[1] => 90
[2] => 120
[3] => 150
[4] => 180
[5] => 210
[6] => 240
[7] => 270
[8] => 300
[9] => 330
)
Array
(
[0] => 0
[1] => 90
[2] => 180
[3] => 270
[4] => 360
)
Array
(
[0] => 60000000
[1] => 62000000
[2] => 64000000
[3] => 66000000
[4] => 68000000
[5] => 70000000
[6] => 72000000
[7] => 74000000
)
мой босс будет доволен этим - тоже спасибо и от меня СПАСИБО !!
Отличный ответ! Конвертирую в Swift 4stackoverflow.com/a/55151115/2670547
@Scott Guthrie: Это замечательно, если входные данные не являются целыми числами и представляют собой небольшие числа, например, если yMin = 0,03 и yMax = 0,11.
это работает как шарм, если вы хотите 10 шагов + ноль
//get proper scale for y
$maximoyi_temp= max($institucion); //get max value from data array
for ($i=10; $i< $maximoyi_temp; $i=($i*10)) {
if (($divisor = ($maximoyi_temp / $i)) < 2) break; //get which divisor will give a number between 1-2
}
$factor_d = $maximoyi_temp / $i;
$factor_d = ceil($factor_d); //round up number to 2
$maximoyi = $factor_d * $i; //get new max value for y
if ( ($maximoyi/ $maximoyi_temp) > 2) $maximoyi = $maximoyi /2; //check if max value is too big, then split by 2
Я все еще борюсь с этим :)
Исходный ответ Gamecat, кажется, работает большую часть времени, но попробуйте подключить, скажем, «3 тика» в качестве необходимого количества тиков (для тех же значений данных 15, 234, 140, 65, 90) .... кажется, дает диапазон тиков 73, который после деления на 10 ^ 2 дает 0,73, что соответствует 0,75, что дает «хороший» диапазон тиков 75.
Затем вычисляем верхнюю границу: 75 * круг (1 + 234/75) = 300
и нижняя граница: 75 * раунд (15/75) = 0
Но ясно, что если вы начнете с 0 и продолжите с шагом 75 до верхней границы 300, вы получите 0,75,150,225,300. .... что, без сомнения, полезно, но это 4 отметки (не включая 0), а не 3 обязательных отметки.
Просто расстраивает то, что он не работает в 100% случаев ... что, конечно же, может быть связано с моей ошибкой!
Первоначально думали, что проблема может быть связана с предложенным Брайаном методом получения x, но это, конечно, совершенно точно.
На основе алгоритма @ Gamecat я создал следующий вспомогательный класс
public struct Interval
{
public readonly double Min, Max, TickRange;
public static Interval Find(double min, double max, int tickCount, double padding = 0.05)
{
double range = max - min;
max += range*padding;
min -= range*padding;
var attempts = new List<Interval>();
for (int i = tickCount; i > tickCount / 2; --i)
attempts.Add(new Interval(min, max, i));
return attempts.MinBy(a => a.Max - a.Min);
}
private Interval(double min, double max, int tickCount)
{
var candidates = (min <= 0 && max >= 0 && tickCount <= 8) ? new[] {2, 2.5, 3, 4, 5, 7.5, 10} : new[] {2, 2.5, 5, 10};
double unroundedTickSize = (max - min) / (tickCount - 1);
double x = Math.Ceiling(Math.Log10(unroundedTickSize) - 1);
double pow10X = Math.Pow(10, x);
TickRange = RoundUp(unroundedTickSize/pow10X, candidates) * pow10X;
Min = TickRange * Math.Floor(min / TickRange);
Max = TickRange * Math.Ceiling(max / TickRange);
}
// 1 < scaled <= 10
private static double RoundUp(double scaled, IEnumerable<double> candidates)
{
return candidates.First(candidate => scaled <= candidate);
}
}
Вышеупомянутые алгоритмы не учитывают случай, когда диапазон между минимальным и максимальным значением слишком мал. А что, если эти значения намного больше нуля? Затем у нас есть возможность начать ось Y со значением выше нуля. Кроме того, чтобы наша линия не находилась полностью в верхней или нижней части графика, мы должны дать ей немного «воздуха для дыхания».
Чтобы охватить эти случаи, я написал (на PHP) приведенный выше код:
function calculateStartingPoint($min, $ticks, $times, $scale) {
$starting_point = $min - floor((($ticks - $times) * $scale)/2);
if ($starting_point < 0) {
$starting_point = 0;
} else {
$starting_point = floor($starting_point / $scale) * $scale;
$starting_point = ceil($starting_point / $scale) * $scale;
$starting_point = round($starting_point / $scale) * $scale;
}
return $starting_point;
}
function calculateYaxis($min, $max, $ticks = 7)
{
print "Min = " . $min . "\n";
print "Max = " . $max . "\n";
$range = $max - $min;
$step = floor($range/$ticks);
print "First step is " . $step . "\n";
$available_steps = array(5, 10, 20, 25, 30, 40, 50, 100, 150, 200, 300, 400, 500);
$distance = 1000;
$scale = 0;
foreach ($available_steps as $i) {
if (($i - $step < $distance) && ($i - $step > 0)) {
$distance = $i - $step;
$scale = $i;
}
}
print "Final scale step is " . $scale . "\n";
$times = floor($range/$scale);
print "range/scale = " . $times . "\n";
print "floor(times/2) = " . floor($times/2) . "\n";
$starting_point = calculateStartingPoint($min, $ticks, $times, $scale);
if ($starting_point + ($ticks * $scale) < $max) {
$ticks += 1;
}
print "starting_point = " . $starting_point . "\n";
// result calculation
$result = [];
for ($x = 0; $x <= $ticks; $x++) {
$result[] = $starting_point + ($x * $scale);
}
return $result;
}
Ответ Мультяшный Крайте работает большую часть времени. Но иногда это приводит к избыточному количеству клещей. Он также не будет работать с отрицательными числами. Общий подход к проблеме приемлем, но есть способ лучше справиться с этим. Алгоритм, который вы хотите использовать, будет зависеть от того, что вы действительно хотите получить. Ниже я представляю вам свой код, который я использовал в своей библиотеке JS Ploting. Я тестировал его, и он всегда работает (надеюсь;)). Вот основные шаги:
Давайте начнем. Сначала основные расчеты
var range = Math.abs(xMax - xMin); //both can be negative
var rangeOrder = Math.floor(Math.log10(range)) - 1;
var power10 = Math.pow(10, rangeOrder);
var maxRound = (xMax > 0) ? Math.ceil(xMax / power10) : Math.floor(xMax / power10);
var minRound = (xMin < 0) ? Math.floor(xMin / power10) : Math.ceil(xMin / power10);
Я округляю минимальное и максимальное значения, чтобы быть уверенным на 100%, что мой график будет охватывать все данные. Также очень важно определить значение log10 диапазона, отрицательное или отрицательное, и вычесть 1 позже. В противном случае ваш алгоритм не будет работать для чисел меньше единицы.
var fullRange = Math.abs(maxRound - minRound);
var tickSize = Math.ceil(fullRange / (this.XTickCount - 1));
//You can set nice looking ticks if you want
//You can find exemplary method below
tickSize = this.NiceLookingTick(tickSize);
//Here you can write a method to determine if you need zero tick
//You can find exemplary method below
var isZeroNeeded = this.HasZeroTick(maxRound, minRound, tickSize);
Я использую «красивые галочки», чтобы избежать таких отметок, как 7, 13, 17 и т. д. Метод, который я использую здесь, довольно прост. Также неплохо иметь zeroTick, когда это необходимо. Так сюжет выглядит намного профессиональнее. Вы найдете все методы в конце этого ответа.
Теперь вам нужно вычислить верхнюю и нижнюю границы. Это очень просто с нулевым тиком, но в другом случае потребуется немного больше усилий. Почему? Потому что мы хотим правильно центрировать сюжет в пределах верхней и нижней границы. Взгляни на мой код. Некоторые из переменных определены вне этой области, а некоторые из них являются свойствами объекта, в котором хранится весь представленный код.
if (isZeroNeeded) {
var positiveTicksCount = 0;
var negativeTickCount = 0;
if (maxRound != 0) {
positiveTicksCount = Math.ceil(maxRound / tickSize);
XUpperBound = tickSize * positiveTicksCount * power10;
}
if (minRound != 0) {
negativeTickCount = Math.floor(minRound / tickSize);
XLowerBound = tickSize * negativeTickCount * power10;
}
XTickRange = tickSize * power10;
this.XTickCount = positiveTicksCount - negativeTickCount + 1;
}
else {
var delta = (tickSize * (this.XTickCount - 1) - fullRange) / 2.0;
if (delta % 1 == 0) {
XUpperBound = maxRound + delta;
XLowerBound = minRound - delta;
}
else {
XUpperBound = maxRound + Math.ceil(delta);
XLowerBound = minRound - Math.floor(delta);
}
XTickRange = tickSize * power10;
XUpperBound = XUpperBound * power10;
XLowerBound = XLowerBound * power10;
}
И вот методы, о которых я упоминал ранее, которые вы можете написать самостоятельно, но вы также можете использовать мои
this.NiceLookingTick = function (tickSize) {
var NiceArray = [1, 2, 2.5, 3, 4, 5, 10];
var tickOrder = Math.floor(Math.log10(tickSize));
var power10 = Math.pow(10, tickOrder);
tickSize = tickSize / power10;
var niceTick;
var minDistance = 10;
var index = 0;
for (var i = 0; i < NiceArray.length; i++) {
var dist = Math.abs(NiceArray[i] - tickSize);
if (dist < minDistance) {
minDistance = dist;
index = i;
}
}
return NiceArray[index] * power10;
}
this.HasZeroTick = function (maxRound, minRound, tickSize) {
if (maxRound * minRound < 0)
{
return true;
}
else if (Math.abs(maxRound) < tickSize || Math.round(minRound) < tickSize) {
return true;
}
else {
return false;
}
}
Есть только одна вещь, которой здесь нет. Это «красивые границы». Это нижние границы, которые представляют собой числа, аналогичные числам в «симпатичных галочках». Например, лучше иметь нижнюю границу, начинающуюся с 5 с размером деления 5, чем иметь график, который начинается с 6 с таким же размером деления. Но это мое увольнение, я оставляю это вам.
Надеюсь, это поможет. Ваше здоровье!
Преобразовал этот отвечать в Swift 4
extension Int {
static func makeYaxis(yMin: Int, yMax: Int, ticks: Int = 10) -> [Int] {
var yMin = yMin
var yMax = yMax
var ticks = ticks
// This routine creates the Y axis values for a graph.
//
// Calculate Min amd Max graphical labels and graph
// increments. The number of ticks defaults to
// 10 which is the SUGGESTED value. Any tick value
// entered is used as a suggested value which is
// adjusted to be a 'pretty' value.
//
// Output will be an array of the Y axis values that
// encompass the Y values.
var result = [Int]()
// If yMin and yMax are identical, then
// adjust the yMin and yMax values to actually
// make a graph. Also avoids division by zero errors.
if yMin == yMax {
yMin -= ticks // some small value
yMax += ticks // some small value
}
// Determine Range
let range = yMax - yMin
// Adjust ticks if needed
if ticks < 2 { ticks = 2 }
else if ticks > 2 { ticks -= 2 }
// Get raw step value
let tempStep: CGFloat = CGFloat(range) / CGFloat(ticks)
// Calculate pretty step value
let mag = floor(log10(tempStep))
let magPow = pow(10,mag)
let magMsd = Int(tempStep / magPow + 0.5)
let stepSize = magMsd * Int(magPow)
// build Y label array.
// Lower and upper bounds calculations
let lb = stepSize * Int(yMin/stepSize)
let ub = stepSize * Int(ceil(CGFloat(yMax)/CGFloat(stepSize)))
// Build array
var val = lb
while true {
result.append(val)
val += stepSize
if val > ub { break }
}
return result
}
}
Это замечательно, если только входные данные не являются целыми числами и являются небольшими числами, например, если yMin = 0,03 и yMax = 0,11.
Для тех, кому это нужно в ES5 Javascript, немного поборолись, но вот оно:
var min=52;
var max=173;
var actualHeight=500; // 500 pixels high graph
var tickCount =Math.round(actualHeight/100);
// we want lines about every 100 pixels.
if (tickCount <3) tickCount =3;
var range=Math.abs(max-min);
var unroundedTickSize = range/(tickCount-1);
var x = Math.ceil(Math.log10(unroundedTickSize)-1);
var pow10x = Math.pow(10, x);
var roundedTickRange = Math.ceil(unroundedTickSize / pow10x) * pow10x;
var min_rounded=roundedTickRange * Math.floor(min/roundedTickRange);
var max_rounded= roundedTickRange * Math.ceil(max/roundedTickRange);
var nr=tickCount;
var str = "";
for(var x=min_rounded;x<=max_rounded;x+=roundedTickRange)
{
str+=x+", ";
}
console.info("nice Y axis "+str);
На основе отличного ответа Toon Krijtje.
Это решение основано на найденном мной Пример Java.
const niceScale = ( minPoint, maxPoint, maxTicks) => {
const niceNum = ( localRange, round) => {
var exponent,fraction,niceFraction;
exponent = Math.floor(Math.log10(localRange));
fraction = localRange / Math.pow(10, exponent);
if (round) {
if (fraction < 1.5) niceFraction = 1;
else if (fraction < 3) niceFraction = 2;
else if (fraction < 7) niceFraction = 5;
else niceFraction = 10;
} else {
if (fraction <= 1) niceFraction = 1;
else if (fraction <= 2) niceFraction = 2;
else if (fraction <= 5) niceFraction = 5;
else niceFraction = 10;
}
return niceFraction * Math.pow(10, exponent);
}
const result = [];
const range = niceNum(maxPoint - minPoint, false);
const stepSize = niceNum(range / (maxTicks - 1), true);
const lBound = Math.floor(minPoint / stepSize) * stepSize;
const uBound = Math.ceil(maxPoint / stepSize) * stepSize;
for(let i=lBound;i<=uBound;i+=stepSize) result.push(i);
return result;
};
console.info(niceScale(15,234,6));
// > [0, 100, 200, 300]
Здесь есть некоторая информация о том, как Excel выбирает максимальные и минимальные значения для своей оси Y: support.microsoft.com/kb/214075