algorithm - tabla - Elegir una escala lineal atractiva para el eje Y de un gráfico
importancia del uso de escalas adecuadas para los ejes (10)
Estoy escribiendo un poco de código para mostrar un gráfico de barras (o líneas) en nuestro software. Todo va bien Lo que me tiene perplejo es etiquetar el eje Y.
La persona que llama puede decirme cuán finamente quieren que se marque la escala Y, pero parece que estoy atascado en exactamente qué etiquetarlos de una manera "atractiva". No puedo describir "atractivo", y probablemente tampoco tú, pero lo sabemos cuando lo vemos, ¿verdad?
Entonces, si los puntos de datos son:
15, 234, 140, 65, 90
Y el usuario pide 10 etiquetas en el eje Y, un poco de engaño con papel y lápiz se presenta con:
0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250
Así que hay 10 allí (sin incluir 0), el último se extiende un poco más allá del valor más alto (234 <250), y es un incremento "agradable" de 25 cada uno. Si pidieran 8 etiquetas, un incremento de 30 se habría visto bien:
0, 30, 60, 90, 120, 150, 180, 210, 240
Nueve habría sido complicado. Tal vez solo haya usado 8 o 10 y llamarlo lo suficientemente cerca estaría bien. ¿Y qué hacer cuando algunos de los puntos son negativos?
Puedo ver que Excel aborda este problema muy bien.
¿Alguien sabe un algoritmo de propósito general (incluso alguna fuerza bruta está bien) para resolver esto? No tengo que hacerlo rápido, pero debería verse bien.
Aquí hay un ejemplo de PHP que estoy usando. Esta función devuelve una matriz de valores bonitos del eje Y que abarcan los valores mínimo y máximo Y pasados. Por supuesto, esta rutina también se podría usar para los valores del eje X.
Le permite "sugerir" cuántas marcas desea, pero la rutina devolverá lo que se ve bien. He agregado algunos datos de muestra y he mostrado los resultados de estos.
#!/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);
?>
Resultado resultado de datos de muestra
# ./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
)
Basado en el algoritmo de @ Gamecat, produje la siguiente clase de ayuda
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);
}
}
Gracias por la pregunta y respuesta, muy útil. Gamecat, me pregunto cómo estás determinando a qué se debe redondear el rango de tics.
marque el rango = 21.9. Esto debería ser 25.0
Para hacer esto algorítmicamente, uno debería agregar lógica al algoritmo anterior para hacer esta escala muy bien para números más grandes. Por ejemplo, con 10 tics, si el rango es 3346, el rango de ticks se evaluaría a 334.6 y el redondeo al 10 más cercano daría 340 cuando 350 es probablemente mejor.
¿Qué piensas?
La respuesta de funciona la mayor parte del tiempo. Pero a veces producirá un exceso de garrapatas. No funcionará con números negativos también. El enfoque general del problema está bien, pero hay una mejor manera de manejar esto. El algoritmo que desee utilizar dependerá de lo que realmente desee obtener. A continuación te presento mi código que utilicé en mi biblioteca JS Ploting. Lo probé y siempre funciona (con suerte;)). Estos son los pasos principales:
- Obtiene los extremos globales xMin y xMax (inluce todos los gráficos que quieras imprimir en el algoritmo)
- calcular el rango entre xMin y xMax
- calcule el orden de magnitud de su rango
- calcule el tamaño de la garrapata dividiendo el rango por el número de garrapatas menos uno
- este es opcional Si desea que la marca cero esté siempre impresa, use el tamaño de tilde para calcular la cantidad de tics positivos y negativos. El número total de tics será su suma + 1 (la marca del cero)
- este no es necesario si tienes cero tick siempre impreso. Calcula el límite inferior y superior, pero recuerda centrar la trama
Empecemos. Primero los cálculos básicos
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);
Redondeo los valores mínimos y máximos para estar 100% seguro de que mi trazado cubrirá todos los datos. También es muy importante registrar el piso10 del rango si es negativo o no y restar 1 más tarde. De lo contrario, su algoritmo no funcionará para números menores a uno.
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);
Utilizo "tics agradables" para evitar tics como 7, 13, 17, etc. El método que uso aquí es bastante simple. También es bueno tener zeroTick cuando sea necesario. Parcela se ve mucho más profesional de esta manera. Encontrarás todos los métodos al final de esta respuesta.
Ahora debes calcular los límites superior e inferior. Esto es muy fácil con cero tics, pero requiere un poco más de esfuerzo en otro caso. ¿Por qué? Porque queremos centrar la trama dentro del límite superior e inferior muy bien. Eche un vistazo a mi código. Algunas de las variables se definen fuera de este ámbito y algunas de ellas son propiedades de un objeto en el que se guarda el código completo presentado.
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;
}
Y aquí hay métodos que mencioné antes, los cuales puede escribir usted mismo, pero también puede usar los míos
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;
}
}
Solo hay una cosa más que no está incluida aquí. Este es el "buen aspecto de los límites". Estos son límites inferiores que son números similares a los números en "ticks agradables". Por ejemplo, es mejor tener el límite inferior comenzando en 5 con el tamaño de tilde 5 que tener un gráfico que comienza en 6 con el mismo tamaño de tilde. Pero este mi despedido te lo dejo a ti.
Espero eso ayude. ¡Aclamaciones!
Los algoritmos anteriores no toman en consideración el caso cuando el rango entre el valor mínimo y máximo es demasiado pequeño. ¿Y qué pasa si estos valores son mucho más altos que cero? Entonces, tenemos la posibilidad de comenzar el eje y con un valor superior a cero. Además, para evitar que nuestra línea esté completamente en el lado superior o inferior del gráfico, debemos darle un poco de "aire para respirar".
Para cubrir esos casos, escribí (en PHP) el código anterior:
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;
}
Oh, hace mucho tiempo, O escribí un módulo gráfico que cubrió esto muy bien. Cavar en la masa gris obtiene lo siguiente:
- Determine el límite inferior y superior de los datos. (¡Cuidado con el caso especial donde el límite inferior = límite superior!
- Divida el rango en la cantidad requerida de tics.
- Redondea el rango de ticks en buenas cantidades.
- Ajuste el límite inferior y superior en consecuencia.
Tomemos su ejemplo:
15, 234, 140, 65, 90 with 10 ticks
- límite inferior = 15
- límite superior = 234
- rango = 234-15 = 219
- marque el rango = 21.9. Esto debería ser 25.0
- nuevo límite inferior = 25 * redondo (15/25) = 0
- nuevo límite superior = 25 * redondo (1 + 235/25) = 250
Entonces el rango = 0,25,50, ..., 225,250
Puedes obtener el buen rango de ticks con los siguientes pasos:
- divida por 10 ^ x de manera que el resultado se encuentre entre 0.1 y 1.0 (incluyendo 0.1 excluyendo 1).
- traducir en consecuencia:
- 0.1 -> 0.1
- <= 0.2 -> 0.2
- <= 0.25 -> 0.25
- <= 0.3 -> 0.3
- <= 0.4 -> 0.4
- <= 0.5 -> 0.5
- <= 0.6 -> 0.6
- <= 0.7 -> 0.7
- <= 0.75 -> 0.75
- <= 0.8 -> 0.8
- <= 0.9 -> 0.9
- <= 1.0 -> 1.0
- multiplicar por 10 ^ x.
En este caso, 21.9 está dividido por 10 ^ 2 para obtener 0.219. Esto es <= 0.25, así que ahora tenemos 0.25. Multiplicado por 10 ^ 2 esto da 25.
Echemos un vistazo al mismo ejemplo con 8 ticks:
15, 234, 140, 65, 90 with 8 ticks
- límite inferior = 15
- límite superior = 234
- rango = 234-15 = 219
- marque el rango = 27.375
- Divida por 10 ^ 2 para 0.27375, se traduce a 0.3, lo que da (multiplicado por 10 ^ 2) 30.
- nuevo límite inferior = 30 * redondo (15/30) = 0
- nuevo límite superior = 30 * redondo (1 + 235/30) = 240
Que dan el resultado que solicitaste ;-).
------ Agregado por KD ------
Aquí hay un código que logra este algoritmo sin usar tablas de búsqueda, etc ...:
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;
En términos generales, el número de tics incluye el tic del fondo, por lo que los segmentos reales del eje y son uno menos que el número de tics.
Parece que la persona que llama no le dice los rangos que quiere.
Por lo tanto, puede cambiar los puntos finales hasta que el recuento de etiquetas lo divida fácilmente.
Vamos a definir "agradable". Yo llamaría bueno si las etiquetas están apagadas por:
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, ...
Encuentra el máximo y el mínimo de tu serie de datos. Llamemos a estos puntos:
min_point and max_point.
Ahora todo lo que necesita hacer es encontrar 3 valores:
- 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"
que se ajustan a la ecuación:
(end_label - start_label)/label_offset == label_count
Probablemente haya muchas soluciones, así que solo elija una. La mayoría de las veces apuesto a que puedes establecer
start_label to 0
así que intente un número entero diferente
end_label
hasta que el desplazamiento sea "bueno"
Prueba este código Lo he usado en algunos escenarios de gráficos y funciona bien. También es bastante rápido.
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;
}
}
Todavía estoy batallando con esto :)
La respuesta original de Gamecat parece funcionar la mayor parte del tiempo, pero intente conectar, por ejemplo, "3 ticks" como la cantidad de tics necesarios (para los mismos valores de datos 15, 234, 140, 65, 90) ... parece dar un rango de tic de 73, que después de dividir por 10 ^ 2 rinde 0,73, que se asigna a 0,75, lo que da un rango de tilde "agradable" de 75.
Luego calculando el límite superior: 75 * redondo (1 + 234/75) = 300
y el límite inferior: 75 * redondo (15/75) = 0
Pero claramente si comienzas en 0, y sigues en pasos de 75 hasta el límite superior de 300, terminas con 0,75,150,225,300 ... lo cual es sin duda útil, pero son 4 tics (sin incluir 0), no el Se requieren 3 tics.
Es frustrante que no funcione el 100% del tiempo ... ¡lo que podría deberse a mi error en alguna parte, por supuesto!
esto funciona como un encanto, si quieres 10 pasos + cero
//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