Mapping Linear Scales From One to Another

Mapping Linear Scales From One to Another

In this post I explain mapping number scales and how to write simple function to convert from one scale to another.

Every time I find myself needing to do this, I have to spend time reasoning my way through it. It's been a number of years since my last proper math class (Finite Field Theory) and a lot longer since high school algebra. As a developer these days, there are few opportunities to utilize those years of Math and when I do, the bigger challenge is distilling a problem down into a math problem. 😊

The Problem

I have two scales of continuous numbers - both with a MIN and MAX value. I need to map one value on to another, but how?

For illustration sake, we might want to create a visualization of the age of adults in a particular population set. We will want to control the opacity of the element representing a person based on their age. If we define an "adult" as a person between the age of 18 and 117, then our age series is the range [18, 117]. Finally, opacity in CSS is a numeric value between 0 and 1. However, for visibility sake, I might want the min opacity to be .25.

Using these scales, I know I want 18 year olds to have opacity of .25 (the respective minimums of the scales) and 117 year olds to have 1.0 opacity (the respective maximums of the scales). I also know that the midpoint of the age range should be approximately .5 opacity. However, this is usually where I need to think things out.

Proportion of Scales

To tackle this problem, I first think about proportions and ratios. Two widescreen tvs are, by definition, both 16:9 aspect ratio regardless of the actual size of the tv. The height and width of one widescreen tv is proportionally the same as another widescreen tv. If I didn't know the 16:9 value, I could derive it by using the known dimensions of the two tvs. Similarly, I can find the proportion of two scales using their min and max values. To do this, I need to divide the difference of the desired output scale (Δout ) by the difference of the input scale(Δin).

Using the example scenario:

proportion = Δout/Δin  
proportion = (1 - .25)/(117-18) => .75/99 = 0.0076 (arbitrarily rounded to nearest ten thousandths place)

If this looks like the formula for finding the slope of a straight line passing through two coordinates on an 2D Cartesian graph, it should.

Starting Points

Now that I have the proportion of the two scales, I need to think about the relative starts to the scales. Whatever I'm given for an input age (x) should be relative to the age scale's MIN value. I can't simply multiply x by the proportion as enticing as that first seems. I will want to subtract the MIN value (18) from the input value before applying the proportion above.

(x - minAge) * proportion => (x - 18) * 0.0076

Once I have this value, I need to make it relative to the MIN of my output scale (opacity) by adding the MIN of that the above value.

((x - minAge) * proportion) - minOpacity => ((x - 18) * 0.0076) - .25

Note: Logically, if multiplying the proportion by the relative x value produces 0 and 0 isn't in our range of values, I need to add the output MIN value to get back to the "bottom of the scale".

🧠 If you recognize this as the point/slope formula, congratulations.

y − y1 = m(x − x1)

It makes sense if you treat each scale as an axis of a 2D Cartesian graph. Then you can apply all the same logic you could with 2D graphs. The MIN and MAX just happen to be two points we can use to generate the point slope formula. Assuming our scales are linear, any known points will work.

Code Solution (TypeScript)

The function below takes in two scales and returns a function you can use to calculate the input arg to the output scale. The multiplication by 1.0 is to ensure that calculations are using floating point math.

type Scale = [number, number];
const scaleMapper = (sOut: Scale, sIn: Scale) => {
  const m = (1.0 * sOut[1] - sOut[0]) / (sIn[1] - sIn[0]);
  return (x: number) => sOut[0] + (m * (x - sIn[0]));
};

let ageToOpacity = scaleMapper([.25, 1.0], [18, 117])

console.log('18 =>', ageToOpacity(18)); // 18 =>,  0.25
console.log('37 =>', ageToOpacity(37)); // 37 =>, 0.3939393
console.log('50 =>', ageToOpacity(50)); // 50 =>, 0.4924242
console.log('100 =>', ageToOpacity(100)); // 100 =>, 0.87121212
console.log('117 =>', ageToOpacity(117)); // 117 =>,  1

Conclusion

Sometimes the solution to your problem is already in your head, but just not recognizable as such. The point slope formula works perfectly for mapping one scale of numbers to another. This is exactly what is needed if the data is linear and continuous. In another post, I will address the same problem, but with discrete data and non-linear data.

Check out my series: Applied Math for other ways math pops up in software development.

Image Credit: Photo by Tim Mossholder from Pexels