Best algorithm for calculating rounded values ​​from a range

I draw a graph where there is time on the horizontal axis and price on the vertical axis.

The price can vary from 0.23487 to 0.8746 or from 20.47 to 45.48 or 1.4578 to 1.6859 or from 9000 to 12000 ... you have an idea, any range can be there. Also, the accuracy of numbers may vary (but usually it is 2 decimal places or 4 decimal places).

Now I need to show prices along the vertical axis, but not all of them are just some significant levels. I need to show as many significant levels as possible, but these levels should not be closer to each other than 30 pixels (.

So, if I have a chart with data whose prices range from 1.4567 to 1.6789 and the chart is 500, I can show a maximum of 16 significant levels. Visible price range: 1.6789-1.4567 = 0.2222. 0.2222 / 16 = 0.0138, so I can show the levels 1.4716, 1.4854, etc. But I want to round these levels to some significant number, for example. 1.4600, 1.4700, 1.4800 ... or 1.4580, 1.4590, 1.4600 ... or 1.4580, 1.4585 ... etc. Therefore, I want to always show as many significant levels as possible, depending on how much space I have, but always only show levels for some significant values ​​(I'm not talking about round values, like 20.25), which are 1, 2 , 2,5, 5 and 10 or their factors (10, 20, 25 ... or 100, 200, 250 ...) or their division (0,1, 0,2, 0,25 ... or 0 , 0001, 0.0002, 0.00025 ...)

I actually got this job, but I don’t like my algorithm at all, it is too long and not elegant. Hope someone can suggest a more elegant and general way. I am looking for an algorithm that I can optionally implement. Below is my current alotism in objective-c. Thanks.

-(float) getPriceLineDenominator { NSArray *possVal = [NSArray arrayWithObjects: [NSNumber numberWithFloat:1.0], [NSNumber numberWithFloat:2.0], [NSNumber numberWithFloat:2.5], [NSNumber numberWithFloat:5.0], [NSNumber numberWithFloat:10.0], [NSNumber numberWithFloat:20.0], [NSNumber numberWithFloat:25.0], [NSNumber numberWithFloat:50.0], [NSNumber numberWithFloat:100.0], nil]; float diff = highestPrice-lowestPrice;//range of shown values double multiplier = 1; if(diff<10) { while (diff<10) { multiplier/=10; diff = diff*10; } } else { while (diff>100) { multiplier*=10; diff = diff/10; } } float result = 0; for(NSNumber *n in possVal) { float f = [n floatValue]*multiplier; float x = [self priceY:highestPrice]; float y = [self priceY:highestPrice-f]; if((yx)>=30)//30 is minimum distance between price levels shown { result = f; break; } } return result; } 
+4
source share
2 answers

You can use logarithms to determine the size of each subband.

Say you know the minimum and maximum values ​​in your data. You also know how many levels you want.

The difference between the maximum and minimum divided by the number of levels is (slightly) less than the size of each subband

 double diff = highestPrice - lowestPrice; // range of shown values double range = diff / levels; // size of range double logrange = log10(range); // log10 int lograngeint = (int)logrange; // integer part double lograngerest = logrange - lograngeint; // fractional part if (lograngerest < 0) { // adjust if negative lograngerest += 1; lograngeint -= 1; } /* now you can increase lograngerest to the boundaries you like */ if (lograngerest < log10(2)) lograngerest = log10(2); else if (lograngerest < log10(2.5)) lograngerest = log10(2.5); else if (lograngerest < log10(5)) lograngerest = log10(5); else lograngerest = /* log10(10) */ 1; /* and the size of each range is therefore */ return pow(10, lograngeint + lograngerest); 

The first range starts a little earlier than the minimum value in the data. Use fmod to find out exactly how much earlier.

+3
source

As you say, the available height determines the maximum number of divisions. For argumentation, avoid magic numbers and say that you have a height pixels and a minimum interval of closest :

 int maxDivs = height / closest; 

Divide the range into this many sections. You will most likely get some ugly value, but it gives you a starting point:

 double minTickSpacing = diff/maxDivs; 

You need to move from this interval until you reach one of your “significant” values ​​in the appropriate order. Instead of looping and dividing / multiplying, you can use some math functions to find order:

 double multiplier = pow(10, -floor(log10(minTickSpacing))); 

Select the following interval from the range {2, 2.5, 5, 10} - I will just do it with constants and if - else for simplicity:

 double scaledSpacing = multiplier * minTickSpacing; if ( scaledSpacing < 2 ) result = 2; else if ( scaledSpacing < 2.5 ) result = 2.5; else if ( scaledSpacing < 5 ) result = 5; else result = 10; return result/multiplier; 

Or something like that. Completely untested, so you will need to check signs and ranges, etc. And there will certainly be some interesting regional cases. But I think it should be in the right step ...

+1
source

All Articles