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 dejó perplejo es etiquetar el eje Y.
La persona que llama puede decirme con qué precisión quieren que se etiquete la escala Y, pero parece que estoy atascado en exactamente qué etiquetarlos de una manera "atractiva". No puedo describir "atractivo", y probablemente tú tampoco, 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 manipulación con papel y lápiz se obtiene:
0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250
Entonces 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 conoce un algoritmo de propósito general (incluso algo de fuerza bruta está bien) para resolver esto? No tengo que hacerlo rápido, pero debería verse bien.
Respuestas:
Hace mucho tiempo, escribí un módulo de gráficos que cubría esto muy bien. Excavar en la masa gris obtiene lo siguiente:
Tomemos tu ejemplo:
Entonces el rango = 0,25,50, ..., 225,250
Puede obtener el buen rango de ticks con los siguientes pasos:
En este caso, 21,9 se divide por 10 ^ 2 para obtener 0,219. Esto es <= 0,25, por lo que ahora tenemos 0,25. Multiplicado por 10 ^ 2, da 25.
Echemos un vistazo al mismo ejemplo con 8 ticks:
Que dan el resultado que solicitaste ;-).
------ Agregado por KD ------
Aquí está el código que logra este algoritmo sin usar tablas de búsqueda, etc ...:
En términos generales, la cantidad de marcas incluye la marca inferior, por lo que los segmentos reales del eje y son uno menos que la cantidad de marcas.
fuente
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 Y mínimo y máximo pasados. Por supuesto, esta rutina también podría usarse para valores del eje X.
Le permite "sugerir" cuántos tics puede querer, pero la rutina devolverá lo que se ve bien. Agregué algunos datos de muestra y mostré los resultados para 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); ?>
Salida de resultado de datos de muestra
fuente
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; } }
fuente
Parece que la persona que llama no le dice los rangos que quiere.
Por lo tanto, puede cambiar los puntos finales hasta que sea divisible por el recuento de etiquetas.
Definamos "agradable". Llamaría agradable si las etiquetas están desactivadas por:
Encuentre el máximo y el mínimo de su serie de datos. Llamemos a estos puntos:
Ahora todo lo que necesita hacer es encontrar 3 valores:
que se ajusta a la ecuación:
Probablemente haya muchas soluciones, así que elija una. La mayor parte del tiempo apuesto a que puedes configurar
así que prueba un entero diferente
hasta que el desplazamiento sea "agradable"
fuente
Todavía estoy luchando con esto :)
La respuesta original de Gamecat parece funcionar la mayor parte del tiempo, pero intente conectar, por ejemplo, "3 ticks" como el número de ticks requerido (para los mismos valores de datos 15, 234, 140, 65, 90) .... parece dar un rango de ticks de 73, que después de dividir por 10 ^ 2 da 0,73, que se asigna a 0,75, lo que da un rango de ticks 'agradable' de 75.
Luego calculando el límite superior: 75 * round (1 + 234/75) = 300
y el límite inferior: 75 * round (15/75) = 0
Pero claramente si comienzas en 0 y continúas en pasos de 75 hasta el límite superior de 300, terminas con 0,75,150,225,300 ... lo que sin duda es útil, pero son 4 tics (sin incluir 0) no el Se requieren 3 tics.
Simplemente frustrante porque no funciona el 100% del tiempo ... ¡lo que podría deberse a mi error en algún lugar, por supuesto!
fuente
La respuesta de Toon Krijthe funciona la mayor parte del tiempo. Pero a veces producirá un número excesivo de garrapatas. Tampoco funcionará con números negativos. El enfoque general del problema está bien, pero hay una mejor manera de manejarlo. El algoritmo que desee utilizar dependerá de lo que realmente desee obtener. A continuación, les presento mi código que usé en mi biblioteca JS Ploting. Lo he probado y siempre funciona (con suerte;)). Estos son los pasos principales:
Empecemos. Primero los cálculos básicos
Redondeo los valores mínimo y máximo para estar 100% seguro de que mi gráfico cubrirá todos los datos. También es muy importante registrar el piso 10 del rango si es negativo o no y restar 1 más tarde. De lo contrario, su algoritmo no funcionará para números menores que uno.
Utilizo "garrapatas bonitas" para evitar garrapatas como 7, 13, 17, etc. El método que utilizo aquí es bastante simple. También es bueno tener zeroTick cuando sea necesario. La trama se ve mucho más profesional de esta manera. Encontrará todos los métodos al final de esta respuesta.
Ahora tienes que calcular los límites superior e inferior. Esto es muy fácil con cero tick pero requiere un poco más de esfuerzo en otro caso. ¿Por qué? Porque queremos centrar bien la trama dentro de los límites superior e inferior. 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 todo el código presentado.
Y aquí hay métodos que mencioné antes que puede escribir usted mismo pero también puede usar el mío
Solo hay una cosa más que no está incluida aquí. Estos son los "lindos límites". Estos son límites inferiores que son números similares a los números de "tics bonitos". Por ejemplo, es mejor tener el límite inferior comenzando en 5 con un tamaño de tick 5 que tener una gráfica que comience en 6 con el mismo tamaño de tick. Pero este mi despedido te lo dejo.
Espero eso ayude. ¡Salud!
fuente
Convirtió esta respuesta como 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 } }
fuente
esto funciona como un encanto, si quieres 10 pasos + cero
fuente
Para cualquiera que necesite esto en ES5 Javascript, ha estado luchando un poco, pero aquí está:
Basado en la excelente respuesta de Toon Krijtje.
fuente
Esta solución se basa en un ejemplo de Java que encontré.
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.log(niceScale(15,234,6)); // > [0, 100, 200, 300]
fuente
Gracias por la pregunta y la respuesta, muy útil. Gamecat, me pregunto cómo está determinando a qué se debe redondear el rango de ticks.
Para hacer esto algorítmicamente, ¿uno tendría que agregar lógica al algoritmo anterior para hacer que esta escala sea adecuada para números más grandes? Por ejemplo, con 10 ticks, si el rango es 3346, entonces el rango de ticks se evaluaría a 334.6 y redondeando a los 10 más cercanos daría 340 cuando 350 es probablemente más agradable.
¿Qué piensas?
fuente
Basado en el algoritmo de @ Gamecat, produje la siguiente clase de ayuda
fuente
Los algoritmos anteriores no tienen en cuenta el caso en el que el rango entre el valor mínimo y máximo es demasiado pequeño. ¿Y 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, tenemos que darle un poco de "aire para respirar".
Para cubrir esos casos, escribí (en PHP) el código anterior:
fuente