J'écris un peu de code pour afficher une barre graphique (ou une ligne) dans notre logiciel. Tout va bien. Ce qui me stoppe, c'est le marquage de l'axe des ordonnées.
L’appelant peut me dire à quel point il souhaite que l’échelle Y soit étiquetée, mais il me semble que je ne sais pas trop comment l’appeler de manière «attrayante». Je ne peux pas décrire "attrayant", et probablement vous non plus, mais nous le savons quand nous le voyons, non?
Donc, si les points de données sont:
15, 234, 140, 65, 90
Et l’utilisateur demande 10 étiquettes sur l’axe des ordonnées. Un peu de papier et un crayon apparaissent:
0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250
Donc il y en a 10 (non compris 0), le dernier s'étend juste au-delà de la valeur la plus élevée (234 <250), et il s’agit d’un incrément "sympa" de 25 chacun. S'ils avaient demandé 8 étiquettes, une augmentation de 30 aurait semblé bien:
0, 30, 60, 90, 120, 150, 180, 210, 240
Neuf aurait été délicat. Peut-être juste avoir utilisé soit 8 ou 10 et l'appeler suffisamment proche serait bien. Et que faire lorsque certains points sont négatifs?
Je peux voir qu'Excel résout ce problème bien.
Est-ce que quelqu'un connaît un algorithme à usage général (même une force brute est acceptable) pour résoudre ce problème? Je n'ai pas à le faire rapidement, mais ça devrait avoir l'air gentil.
Il y a longtemps, j'ai écrit un module graphique qui couvrait bien cette question. Creuser dans la masse grise donne les résultats suivants:
Prenons votre exemple:
15, 234, 140, 65, 90 with 10 ticks
Donc, la plage = 0,25,50, ..., 225,250
Vous pouvez obtenir la plage de ticks de Nice en procédant comme suit:
Dans ce cas, 21.9 est divisé par 10 ^ 2 pour obtenir 0.219. Ceci est <= 0.25, nous avons donc 0.25. Multiplié par 10 ^ 2, cela donne 25.
Jetons un coup d'oeil au même exemple avec 8 ticks:
15, 234, 140, 65, 90 with 8 ticks
Lesquels donnent le résultat que vous avez demandé ;-).
------ Ajouté par KD ------
Voici le code qui réalise cet algorithme sans utiliser de tables de recherche, 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 règle générale, le nombre de ticks inclut le tick inférieur, de sorte que les segments d'axe des ordonnées réels sont inférieurs de un au nombre de ticks.
Voici un exemple PHP que j'utilise. Cette fonction renvoie un tableau de jolies valeurs sur l’axe Y, qui comprend les valeurs Y min et max qui ont été entrées. Bien entendu, cette routine peut également être utilisée pour les valeurs de l’axe X.
Cela vous permet de "suggérer" le nombre de ticks que vous voudrez peut-être, mais la routine retournera. J'ai ajouté des exemples de données et présenté les résultats.
#!/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);
?>
Résultat obtenu à partir des données de l'échantillon
# ./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
)
Essayez ce code. Je l'ai utilisé dans quelques scénarios de cartographie et cela fonctionne bien. C'est assez rapide aussi.
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;
}
}
On dirait que l'appelant ne vous dit pas les gammes qu'il veut.
Vous êtes donc libre de modifier les points de terminaison jusqu'à ce que vous obteniez une bonne division par le nombre d'étiquettes.
Définissons "Nice". J'appellerais Nice si les étiquettes sont éteintes par:
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, ...
Trouvez le max et le min de votre série de données. Appelons ces points:
min_point and max_point.
Maintenant, tout ce que vous avez à faire est de trouver 3 valeurs:
- 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"
qui correspondent à l'équation:
(end_label - start_label)/label_offset == label_count
Il y a probablement beaucoup de solutions, alors choisissez-en une. La plupart du temps, je parie que vous pouvez définir
start_label to 0
alors essayez simplement un entier différent
end_label
jusqu'à ce que l'offset soit "Nice"
Je me bats toujours avec ça :)
La réponse Gamecat originale semble fonctionner la plupart du temps, mais essayez de la connecter en indiquant "3 ticks" comme nombre de ticks requis (pour les mêmes valeurs de données 15, 234, 140, 65, 90) semble donner une plage de ticks de 73, ce qui après avoir divisé par 10 ^ 2 donne 0,73, ce qui correspond à 0,75, ce qui donne une plage de ticks "de Nice" de 75.
Puis calculant la limite supérieure: 75 * round (1 + 234/75) = 300
et la limite inférieure: 75 * round (15/75) = 0
Mais évidemment, si vous commencez à 0 et que vous avancez par pas de 75 jusqu'à la limite supérieure de 300, vous vous retrouvez avec 0,75,150,225,300 .... ce qui est sans doute utile, ) pas les 3 ticks requis.
Juste frustrant que ça ne marche pas 100% du temps ... ce qui pourrait bien être dû à mon erreur quelque part bien sûr!
La réponse de Toon Krijthe fonctionne la plupart du temps. Mais parfois, cela produira un nombre excessif de tiques. Cela ne fonctionnera pas aussi avec les nombres négatifs. L’approche globale du problème est acceptable mais il existe un meilleur moyen de le gérer. L'algorithme que vous souhaitez utiliser dépendra de ce que vous voulez vraiment obtenir. Ci-dessous, je vous présente mon code que j'ai utilisé dans ma bibliothèque JS Ploting. Je l'ai testé et ça marche toujours (j'espère;)). Voici les principales étapes:
Commençons. D'abord les calculs de base
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);
J'arrondis les valeurs minimale et maximale pour être sûr à 100% que mon graphique couvrira toutes les données. Il est également très important de garder le log10 au sol de la plage, qu’il soit négatif ou non, et de soustraire 1 plus tard. Sinon, votre algorithme ne fonctionnera pas pour les nombres inférieurs à un.
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);
J'utilise "Nice looking ticks" pour éviter les ticks tels que 7, 13, 17, etc. La méthode que j'utilise ici est assez simple. Il est également agréable d'avoir zeroTick en cas de besoin. La parcelle a l'air beaucoup plus professionnelle de cette façon. Vous trouverez toutes les méthodes à la fin de cette réponse.
Maintenant, vous devez calculer les limites supérieure et inférieure. C'est très facile avec zéro tick mais nécessite un peu plus d'effort dans les autres cas. Pourquoi? Parce que nous voulons bien centrer l'intrigue entre les limites supérieure et inférieure. Regardez mon code. Certaines des variables sont définies en dehors de cette portée et certaines d'entre elles sont des propriétés d'un objet dans lequel tout le code présenté est conservé.
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;
}
Et voici les méthodes que j'ai mentionnées avant que vous pouvez écrire vous-même mais vous pouvez également utiliser le mien
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;
}
}
Il ne reste plus qu’une chose qui n’est pas incluse ici. C'est le "joli regard". Ce sont des bornes inférieures qui sont des nombres similaires aux numéros de "Nice ticks". Par exemple, il est préférable que la limite inférieure commence à 5 avec la taille de tick 5 plutôt que d'avoir un tracé qui commence à 6 avec la même taille de tick. Mais c'est mon viré, je vous le laisse.
J'espère que ça aide. À votre santé!
cela fonctionne comme un charme, si vous voulez 10 étapes + zéro
//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
Converti cette réponse as 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
}
}
Pour ceux qui ont besoin de cela en Javascript ES5, ça fait un peu de la lutte, mais le voici:
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.log("Nice Y axis "+str);
Basé sur l'excellente réponse de Toon Krijtje.
Les algorithmes ci-dessus ne prennent pas en compte le cas où la plage entre les valeurs min et max est trop petite. Et si ces valeurs sont beaucoup plus élevées que zéro? Ensuite, nous avons la possibilité de démarrer l’axe des y avec une valeur supérieure à zéro. De plus, afin d'éviter que notre ligne ne se trouve entièrement en haut ou en bas du graphique, nous devons lui donner un peu d'air pour respirer.
Pour couvrir ces cas, j'ai écrit (sous PHP) le code ci-dessus:
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;
}
Basé sur l'algorithme de @ Gamecat, j'ai produit la classe d'assistance suivante
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);
}
}
Merci pour la question et la réponse, très utile. Gamecat, je me demande comment vous déterminez à quoi la plage de ticks doit être arrondie.
plage de ticks = 21,9. Cela devrait être 25.0
Pour faire cela de manière algorithmique, il faudrait ajouter une logique à l'algorithme ci-dessus pour rendre cette échelle bien pour les grands nombres? Par exemple, avec 10 ticks, si la plage est 3346, la plage de ticks serait évaluée à 334,6 et l’arrondissement au 10 le plus proche donnerait 340 lorsque 350 est probablement plus agréable.
Qu'est-ce que tu penses?