Ich brauche einen ziemlich intelligenten Algorithmus, um "Nice" -Gitterlinien für ein Diagramm (Diagramm) zu erhalten.
Nehmen Sie beispielsweise ein Balkendiagramm mit den Werten 10, 30, 72 und 60 an. Sie wissen:
Minimaler Wert: 10 ... Max. Wert: 72 .... Bereich: 62
Die erste Frage lautet: Womit fangen Sie an? In diesem Fall wäre 0 der intuitive Wert, aber dies hält andere Datensätze nicht aus. Ich vermute also:
Der minimale Rasterwert sollte entweder 0 oder ein "Nice" -Wert sein, der niedriger ist als der minimale Wert der Daten im Bereich. Alternativ kann es angegeben werden.
Der Maximalwert des Gitters sollte ein "Nice" -Wert sein, der über dem Maximalwert im Bereich liegt. Alternativ kann es angegeben werden (z. B. möchten Sie möglicherweise 0 bis 100 angeben, wenn Sie Prozentsätze anzeigen, unabhängig von den tatsächlichen Werten).
Die Anzahl der Gitterlinien (Ticks) im Bereich sollte entweder angegeben werden oder eine Anzahl innerhalb eines bestimmten Bereichs (z. B. 3-8), so dass die Werte "Nizza" (dh runde Zahlen) sind und Sie den Diagrammbereich maximal nutzen. In unserem Beispiel wäre 80 ein vernünftiges Maximum, da dies 90% der Diagrammhöhe (72/80) erfordern würde, während 100 mehr Platz verschwenden würde.
Kennt jemand einen guten Algorithmus dafür? Die Sprache ist irrelevant, da ich sie in das umsetze, was ich brauche.
CPAN bietet eine Implementierung hier (siehe Quelllink)
Siehe auch Tickmark-Algorithmus für eine Diagrammachse
Zu Ihrer Information, mit Ihren Beispieldaten:
Ich habe das mit einer Art Brute-Force-Methode gemacht. Ermitteln Sie zunächst die maximale Anzahl von Markierungen, die Sie in das Feld einfügen können. Dividieren Sie den gesamten Wertebereich durch die Anzahl der Ticks. Dies ist das minimum Abstand der Zecke. Berechnen Sie nun den Boden der Logarithmusbasis 10, um die Größe des Ticks zu erhalten, und dividieren Sie diesen Wert. Sie sollten mit etwas im Bereich von 1 bis 10 enden. Wählen Sie einfach die Rundenummer, die größer oder gleich dem Wert ist, und multiplizieren Sie sie mit dem zuvor berechneten Logarithmus. Dies ist Ihr endgültiger Tickabstand.
Beispiel in 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
Bearbeiten: Sie können die Auswahl der "Nizza" -Intervalle ändern. Ein Kommentator scheint mit der Auswahl unzufrieden zu sein, da die tatsächliche Anzahl der Ticks bis zu 2,5-mal unter dem Maximum liegen kann. Hier ist eine kleine Modifikation, die eine Tabelle für die Nice-Intervalle definiert. Im Beispiel habe ich die Auswahl so erweitert, dass die Anzahl der Ticks nicht weniger als 3/5 des Maximums beträgt.
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
Das Problem besteht aus zwei Teilen:
Sie können den ersten Teil mit Logarithmen behandeln:
range = max - min;
exponent = int(log(range)); // See comment below.
magnitude = pow(10, exponent);
Wenn Ihr Bereich beispielsweise zwischen 50 und 1200 liegt, ist der Exponent 3 und die Stärke 1000.
Dann beschäftigen Sie sich mit dem zweiten Teil, indem Sie entscheiden, wie viele Unterteilungen Sie in Ihrem Raster wünschen:
value_per_division = magnitude / subdivisions;
Dies ist eine grobe Berechnung, da der Exponent auf eine ganze Zahl gekürzt wurde. Möglicherweise möchten Sie die Exponentenberechnung optimieren, um Randbedingungen besser zu verarbeiten, z. durch Runden anstelle von int()
, wenn Sie zu viele Unterteilungen erhalten.
Ich verwende den folgenden Algorithmus. Es ist ähnlich wie bei anderen hier, aber es ist das erste Beispiel in 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;
}
}
Hier ist eine weitere Implementierung in 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;
};
Von Mark oben genommen, eine etwas vollständigere Util-Klasse in c #. Das berechnet auch einen passenden ersten und letzten Tick.
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;
Ich habe eine Objective-C-Methode geschrieben, um eine Nice-Achsenskala und Nice-Ticks für gegebene min- und max-Werte Ihres Datensatzes zurückzugeben:
- (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;
}
Sie können die Methode folgendermaßen aufrufen:
NSArray *niceYAxisValues = [self niceAxis:-maxy :maxy];
und erhalten Sie die Achseneinstellung:
double minYAxisValue = [[niceYAxisValues objectAtIndex:0]doubleValue];
double maxYAxisValue = [[niceYAxisValues objectAtIndex:1]doubleValue];
double ticksYAxis = [[niceYAxisValues objectAtIndex:2]doubleValue];
Nur für den Fall, dass Sie die Anzahl der Achsentexte begrenzen möchten, tun Sie dies:
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)));
Der erste Teil des Codes basiert auf der Berechnung, die ich gefunden habe: here , um die Achsenskala von Nice-Diagramm und die Ticks ähnlich wie in Excel-Diagrammen zu berechnen. Es funktioniert hervorragend für alle Arten von Datensätzen. Hier ist ein Beispiel für eine iPhone-Implementierung:
Ich bin der Autor des " Algorithmus für die optimale Skalierung auf einer Diagrammachse ". Es wurde früher auf trollop.org gehostet, aber ich habe kürzlich Domains/Blogging-Engines verschoben.
Bitte sehen Sie meine Antwort auf eine verwandte Frage .
Eine andere Idee ist, dass der Bereich der Achse der Bereich der Werte ist, die Strichmarkierungen jedoch an der entsprechenden Position gesetzt werden. Das heißt, für 7 bis 22 do:
[- - - | - - - - | - - - - | - -] 10 15 20
Für die Auswahl des Tick-Abstands würde ich eine beliebige Zahl der Form 10 ^ x * i/n vorschlagen, wobei i <n und 0 <n <10 ist. Erstellen Sie diese Liste und sortieren Sie sie, und Sie können die größte Zahl finden kleiner als value_per_division (wie in adam_liss) bei einer binären Suche.
In Python: steps = [numpy.round(x) for x in np.linspace(min, max, num=num_of_steps)]
Wenn Sie versuchen, die Skalen in VB.NET-Diagrammen richtig darzustellen, habe ich das Beispiel von Adam Liss verwendet. Stellen Sie jedoch sicher, dass Sie die Min- und Max-Skalenwerte festlegen, die Sie aus einer Variablen vom Typ decimal übergeben (nicht vom Typ "single" oder "double") Andernfalls werden die Markierungswerte auf 8 Dezimalstellen gesetzt. So hatte ich beispielsweise 1 Diagramm, in dem ich den Wert für die minimale Y-Achse auf 0,0001 und die maximale Y-Achse eingestellt habe Wert auf 0,002 .. Wenn ich diese Werte als Einzelobjekte an das Diagrammobjekt übergeben, bekomme ich Häkchenwerte von 0.00048000001697801, 0.000860000036482233 .... Wenn ich diese Werte jedoch als Dezimalzahlen an das Diagrammobjekt weitergebe, bekomme ich Nice tick Markierungswerte von 0.00048, 0.00086 ......
Hier einige Anregungen aus den hier verfügbaren Antworten. Hier ist meine Implementierung in C. Beachten Sie, dass das Array ndex
eine gewisse Erweiterbarkeit enthält.
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;
}
In R verwenden
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)
}
das Bereichsargument ist max-min der Domäne.
Hier ist eine Javascript-Funktion, die ich geschrieben habe, um die Rasterintervalle (max-min)/gridLinesNumber
auf schöne Werte zu runden. Es funktioniert mit beliebigen Zahlen, siehe Gist mit detaillierten Kommets, um herauszufinden, wie es funktioniert und wie man es anruft.
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
}
Durch Aufruf von ceilAbs(number, [0.5])
für verschiedene Nummern werden Zahlen wie folgt gerundet:
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
Schauen Sie sich die Geige an, um mit dem Code zu experimentieren. Code in der Antwort, der Gist und die Geige unterscheiden sich geringfügig. Ich verwende den in der Antwort angegebenen.