How to scale down a range of numbers with a known min and max value
So I am trying to figure out how to take a range of numbers and scale the values down to fit a range. The reason for wanting to do this is that I am trying to draw ellipses in a java swing jpanel. I want the height and width of each ellipse to be in a range of say 1-30. I have methods that find the minimum and maximum values from my data set, but I won't have the min and max until runtime. Is there an easy way to do this?
Let's say you want to scale a range [min,max]
to [a,b]
. You're looking for a (continuous) function that satisfies
f(min) = a
f(max) = b
In your case, a
would be 1 and b
would be 30, but let's start with something simpler and try to map [min,max]
into the range [0,1]
.
Putting min
into a function and getting out 0 could be accomplished with
f(x) = x - min ===> f(min) = min - min = 0
So that's almost what we want. But putting in max
would give us max - min
when we actually want 1. So we'll have to scale it:
x - min max - min
f(x) = --------- ===> f(min) = 0; f(max) = --------- = 1
max - min max - min
which is what we want. So we need to do a translation and a scaling. Now if instead we want to get arbitrary values of a
and b
, we need something a little more complicated:
(b-a)(x - min)
f(x) = -------------- + a
max - min
You can verify that putting in min
for x
now gives a
, and putting in max
gives b
.
You might also notice that (b-a)/(max-min)
is a scaling factor between the size of the new range and the size of the original range. So really we are first translating x
by -min
, scaling it to the correct factor, and then translating it back up to the new minimum value of a
.
Hope this helps.
Here's some JavaScript for copy-paste ease (this is irritate's answer):
function scaleBetween(unscaledNum, minAllowed, maxAllowed, min, max) {
return (maxAllowed - minAllowed) * (unscaledNum - min) / (max - min) + minAllowed;
}
Applied like so, scaling the range 10-50 to a range between 0-100.
var unscaledNums = [10, 13, 25, 28, 43, 50];
var maxRange = Math.max.apply(Math, unscaledNums);
var minRange = Math.min.apply(Math, unscaledNums);
for (var i = 0; i < unscaledNums.length; i++) {
var unscaled = unscaledNums[i];
var scaled = scaleBetween(unscaled, 0, 100, minRange, maxRange);
console.log(scaled.toFixed(2));
}
0.00, 18.37, 48.98, 55.10, 85.71, 100.00
Edit:
I know I answered this a long time ago, but here's a cleaner function that I use now:
Array.prototype.scaleBetween = function(scaledMin, scaledMax) {
var max = Math.max.apply(Math, this);
var min = Math.min.apply(Math, this);
return this.map(num => (scaledMax-scaledMin)*(num-min)/(max-min)+scaledMin);
}
Applied like so:
[-4, 0, 5, 6, 9].scaleBetween(0, 100);
[0, 30.76923076923077, 69.23076923076923, 76.92307692307692, 100]
For convenience, here is Irritate's algorithm in a Java form. Add error checking, exception handling and tweak as necessary.
public class Algorithms {
public static double scale(final double valueIn, final double baseMin, final double baseMax, final double limitMin, final double limitMax) {
return ((limitMax - limitMin) * (valueIn - baseMin) / (baseMax - baseMin)) + limitMin;
}
}
Tester:
final double baseMin = 0.0;
final double baseMax = 360.0;
final double limitMin = 90.0;
final double limitMax = 270.0;
double valueIn = 0;
System.out.println(Algorithms.scale(valueIn, baseMin, baseMax, limitMin, limitMax));
valueIn = 360;
System.out.println(Algorithms.scale(valueIn, baseMin, baseMax, limitMin, limitMax));
valueIn = 180;
System.out.println(Algorithms.scale(valueIn, baseMin, baseMax, limitMin, limitMax));
90.0
270.0
180.0
Here's how I understand it:
What percent does x
lie in a range
Let's assume you have a range from 0
to 100
. Given an arbitrary number from that range, what "percent" from that range does it lie in? This should be pretty simple, 0
would be 0%
, 50
would be 50%
and 100
would be 100%
.
Now, what if your range was 20
to 100
? We cannot apply the same logic as above (divide by 100) because:
20 / 100
doesn't give us 0
(20
should be 0%
now). This should be simple to fix, we just need to make the numerator 0
for the case of 20
. We can do that by subtracting:
(20 - 20) / 100
However, this doesn't work for 100
anymore because:
(100 - 20) / 100
doesn't give us 100%
. Again, we can fix this by subtracting from the denominator as well:
(100 - 20) / (100 - 20)
A more generalized equation for finding out what % x
lies in a range would be:
(x - MIN) / (MAX - MIN)
Scale range to another range
Now that we know what percent a number lies in a range, we can apply it to map the number to another range. Let's go through an example.
old range = [200, 1000]
new range = [10, 20]
If we have a number in the old range, what would the number be in the new range? Let's say the number is 400
. First, figure out what percent 400
is within the old range. We can apply our equation above.
(400 - 200) / (1000 - 200) = 0.25
So, 400
lies in 25%
of the old range. We just need to figure out what number is 25%
of the new range. Think about what 50%
of [0, 20]
is. It would be 10
right? How did you arrive at that answer? Well, we can just do:
20 * 0.5 = 10
But, what about from [10, 20]
? We need to shift everything by 10
now. eg:
((20 - 10) * 0.5) + 10
a more generalized formula would be:
((MAX - MIN) * PERCENT) + MIN
To the original example of what 25%
of [10, 20]
is:
((20 - 10) * 0.25) + 10 = 12.5
So, 400
in the range [200, 1000]
would map to 12.5
in the range [10, 20]
TLDR
To map x
from old range to new range:
OLD PERCENT = (x - OLD MIN) / (OLD MAX - OLD MIN)
NEW X = ((NEW MAX - NEW MIN) * OLD PERCENT) + NEW MIN
I came across this solution but this does not really fit my need. So I digged a bit in the d3 source code. I personally would recommend to do it like d3.scale does.
So here you scale the domain to the range. The advantage is that you can flip signs to your target range. This is useful since the y axis on a computer screen goes top down so large values have a small y.
public class Rescale {
private final double range0,range1,domain0,domain1;
public Rescale(double domain0, double domain1, double range0, double range1) {
this.range0 = range0;
this.range1 = range1;
this.domain0 = domain0;
this.domain1 = domain1;
}
private double interpolate(double x) {
return range0 * (1 - x) + range1 * x;
}
private double uninterpolate(double x) {
double b = (domain1 - domain0) != 0 ? domain1 - domain0 : 1 / domain1;
return (x - domain0) / b;
}
public double rescale(double x) {
return interpolate(uninterpolate(x));
}
}
And here is the test where you can see what I mean
public class RescaleTest {
@Test
public void testRescale() {
Rescale r;
r = new Rescale(5,7,0,1);
Assert.assertTrue(r.rescale(5) == 0);
Assert.assertTrue(r.rescale(6) == 0.5);
Assert.assertTrue(r.rescale(7) == 1);
r = new Rescale(5,7,1,0);
Assert.assertTrue(r.rescale(5) == 1);
Assert.assertTrue(r.rescale(6) == 0.5);
Assert.assertTrue(r.rescale(7) == 0);
r = new Rescale(-3,3,0,1);
Assert.assertTrue(r.rescale(-3) == 0);
Assert.assertTrue(r.rescale(0) == 0.5);
Assert.assertTrue(r.rescale(3) == 1);
r = new Rescale(-3,3,-1,1);
Assert.assertTrue(r.rescale(-3) == -1);
Assert.assertTrue(r.rescale(0) == 0);
Assert.assertTrue(r.rescale(3) == 1);
}
}