Related
I have a logic problem. I need to distribute an array of values evenly to another array of values. To illustrate:
const colors = ['green', 'yellow', 'red']
const plots = [0, 10, 40, 90, 150, 230, 250]
const withColors = plots.map(e => ({
value: e, color: ???
}))
/* expected output:
[
{value: 0, color: 'green'},
{value: 10, color: 'green'},
{value: 40, color: 'yellow'},
{value: 90, color: 'yellow'},
{value: 150, color: 'red'},
{value: 230, color: 'red'},
{value: 250, color: 'red'},
]
*/
current solution, I definitely have no idea yet, and I will update my question as I am currently brainstorming how to solve this.
Here's a brute force method where I iterate through the array and give each third of the plots a color.
Another way would be to write a helper function that handles turning the values of i/plots.length into 0, 1, or 2 and return that.
const colors = ['green', 'yellow', 'red'];
const plots = [0, 10, 40, 90, 150, 230, 250];
const result = {};
for (var i=0; i<plots.length; i++) {
var thisPlot = plots[i];
if (i / plots.length < 1/3.0) {
result[thisPlot] = colors[0];
}
else if (i / plots.length < 2/3.0) {
result[thisPlot] = colors[1];
}
else {
result[thisPlot] = colors[2];
}
}
console.log(result);
Can't see any logic behind this how this pattern will grow in future, but certainly you can try something like below.
// const colors = ['green', 'yellow', 'red']
const plots = [0, 10, 40, 90, 150, 230, 250]
function getColors(val) {
if(val >= 150 && val <=250) return "red";
if(val >= 40 && val <= 90) return "yellow";
return "green"
}
const withColors = plots.map(e => ({
value: e, color: getColors(e)
}))
Assigning colors one by one to each index
This will spread colors evenly, and is the simplest algorithm, but does not keep the colors grouped as in your example.
const colors = ['green', 'yellow', 'red']
const plots = [0, 10, 40, 90, 150, 230, 250, 260]
const withColors = plots.map((plot, i) => ({ value: plot, color: colors[i % colors.length]}));
console.log(withColors);
Keeping colors grouped
Just calculate the number of plots to map to each color, and assign each color to the appropriate number of plots.
One caveat: We require plots per color to be at least one in the case that there are less plots than colors. In this case, some colors will not be included.
There is one thing you haven't specified: what to do with the remainder. In your example plots.length / colors.length == 7 / 3 is 2 with a remainder of 1. In other words, 2 plots per color, and 1 plot left over. Having a remainder of 1 is easy: just assign any color to an extra plot. But what about larger remainders? There's a few strategies.
Assigning the last color to all trailing plots
Just round down plots per color and keep using the last color for any extra plots.
const colors = ['green', 'yellow', 'red']
const plots = [0, 10, 40, 90, 150, 230, 250, 260]
const plotsPerColor = Math.max(1, Math.floor(plots.length / colors.length));
let colorIdx = 0;
let count = 0;
const withColors = plots.map((plot) => {
const result = { value: plot, color: colors[colorIdx]};
if (++count === plotsPerColor && colorIdx < colors.length-1) {
colorIdx++;
count = 0;
}
return result;
})
console.log(withColors);
Spreading remainder evenly at end
Round down plots per color and increase it by one at the appropriate time.
const colors = ['green', 'yellow', 'red']
const plots = [0, 10, 40, 90, 150, 230, 250, 260]
let plotsPerColor = Math.max(1, Math.floor(plots.length / colors.length));
let remainder = plots.length % colors.length;
let colorIdx = 0;
let count = 0;
const withColors = plots.map((plot) => {
const result = { value: plot, color: colors[colorIdx]};
if (++count === plotsPerColor) {
if (++colorIdx === colors.length - remainder) plotsPerColor++;
count = 0;
}
return result;
});
console.log(withColors);
Spreading remainder evenly at beginning
Round up plots per color and decrease it by one at the appropriate time.
const colors = ['green', 'yellow', 'red']
const plots = [0, 10, 40, 90, 150, 230, 250, 260]
let plotsPerColor = Math.ceil(plots.length / colors.length);
let remainder = plots.length % colors.length;
let colorIdx = 0;
let count = 0;
const withColors = plots.map((plot) => {
const result = { value: plot, color: colors[colorIdx]};
if (++count === plotsPerColor) {
if (++colorIdx === remainder) plotsPerColor--;
count = 0;
}
return result;
});
console.log(withColors);
I have below array
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200]
From the frontEnd user enters any number say it is
const number = 136
I need to find closest to the number but the lesser one.
So the output should be 125
Even if the number is 149 the output should be 125
How can I do this. I tried many way but could get the answer.
Thanks!!!
You can use Array.reduce for this
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200]
function getClosestNumber(d) {
return floorPerDayMilestones.reduce((a, b) => b <=d && a < b ? b : a, 0 )
}
console.log(getClosestNumber(135) || 'No lesser number available')
console.log(getClosestNumber(149) || 'No lesser number available')
console.log(getClosestNumber(22) || 'No lesser number available')
Maybe you should look at this: get closest number out of array
And in your foreach, save your closest number in a var. Then check if your number is bigger than the one in your array . if yes, take your last var, else continue your foreach
You can sort the array in ascending order and then find the index of number that is immediately higher than number then one position less of that value will be the immediate number that is less than number
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200]
const number = 136;
floorPerDayMilestones.sort((a,b)=> a-b);
var index = floorPerDayMilestones.findIndex(val => val>number);
var num = floorPerDayMilestones[index-1];
console.log(num);
Try this
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200];
const number = 136;
const nextLesser = floorPerDayMilestones.reduce((nl, curr) => (curr <= number) && (curr > nl) ? curr : nl , 0)
console.log(nextLesser)
Used Array.prototype.reduce
You can also filter out numbers greater than given and select max from this subset.
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200]
const number = 136
const filtered = floorPerDayMilestones.filter(el=>el<=number);
console.log(Math.max(...filtered))
Or, if you're already using lodash (only in this case - don't import lodash just to use solution below), you can do it with maxBy. maxBy treats numbers greater than number as null.
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200]
const number = 136
let result = _.maxBy(floorPerDayMilestones, el=>el<=number?el:null);
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
With lodash you could use _.sortedIndex:
const numbers = [25, 50, 75, 100, 125, 150, 175, 200]
const closestNum = (arr, n) => {
let index = _.sortedIndex(arr, n)
return arr[index] == n ? arr[index] : arr[index-1]
}
console.log(closestNum(numbers, 135)) // 120
console.log(closestNum(numbers, 160)) // 150
console.log(closestNum(numbers, 180)) // 175
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>
With JS you can simply use Array.reduceRight:
const numbers = [25, 50, 75, 100, 125, 150, 175, 200]
const closestNum = (arr, n) => arr.reduceRight((r,c) => !r && c < n ? c : r, 0)
console.log(closestNum(numbers, 135)) // 120
console.log(closestNum(numbers, 160)) // 150
console.log(closestNum(numbers, 180)) // 175
Since it will start from the right side all you care about is to find the first number less than your argument n.
You could also do Array.reverse and then just Array.filter (use Array.from if you do not want to mutate the array):
const numbers = [25, 50, 75, 100, 125, 150, 175, 200]
const closestNum = (arr, n) => arr.reverse().find(x => x < n)
// To not mutate use `Array.from`
// const closestNum = (arr, n) => Array.from(arr).reverse().find(x => x < n)
console.log(closestNum(numbers, 135)) // 120
console.log(closestNum(numbers, 160)) // 150
console.log(closestNum(numbers, 180)) // 175
If it's sorted in ascending order like in the question this should work.
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200];
const number = 136;
function findClosest(arr, num) {
for (let i = 0; i < arr.length; ++i) {
if (arr[i] > num) {
return arr[i - 1];
}
}
}
console.log(findClosest(floorPerDayMilestones,number));
I have a slider that has the following raw snap points:
[-100, -200, -300, -400, -500, -600]
And I would like to convert the sliding value to match the following snap points:
[0, 5, 10, 25, 50, 100]
A raw value in [-100, -200) should be mapped to a value in [0, 5)
A raw value in [-200, -300) should be mapped to a value in [5, 10)
A raw value in [-300, -400) should be mapped to a value in [10, 25)
And so on ..
How can I achieve that?
Edit: added my attempt (different raw values though)
// sliderValue is an integer obtained from the slider
const base = -70
const offset = -80
const limits = [
base + offset * 0, // -70
base + offset * 1, // -150
base + offset * 2, // -230
base + offset * 3, // -310
base + offset * 4, // -390
base + offset * 5, // -470
]
const points = [0, 5, 10, 25, 50, 100]
// I can't even begin to make sense of this
// don't know I came up with it, but it works ¯\_(ツ)_/¯
if (sliderValue <= limits[4]) {
percentage = scaleValue(sliderValue, limits[4], limits[5], 50, 100)
} else if (sliderValue <= limits[3]) {
percentage = scaleValue(sliderValue, limits[3], limits[4], 25, 50)
} else if (sliderValue <= limits[2]) {
percentage = scaleValue(sliderValue, limits[2], limits[3], 10, 25)
} else if (sliderValue <= limits[1]) {
percentage = scaleValue(sliderValue, limits[1], limits[2], 5, 10)
} else if (sliderValue <= limits[0]) {
percentage = scaleValue(sliderValue, limits[0], limits[1], 0, 5)
}
console.log(percentage)
// ..
function scaleValue(num, in_min, in_max, out_min, out_max) {
return ((num - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min
}
You could take a function with a look up for the section. Then build the new value, based on the four values as a linear function.
function getValue(x) {
var a = [-100, -200, -300, -400, -500, -600],
b = [0, 5, 10, 25, 50, 100],
i = a.findIndex((v, i, a) => v >= x && x >= a[i + 1]);
return [x, (x -a[i])* (b[i + 1] - b[i]) / (a[i + 1] - a[i]) +b[i]].join(' ');
}
console.log([-100, -150, -200, -250, -300, -350, -400, -450, -500, -550, -600].map(getValue));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Simple linear equation: add 100, divide by 20, then negate.
UPDATE: Due to early-morning eye bleariness, I misread the question. (Sorry!) The general method for mapping linear relations to each other is to figure out the offset of the two sets and the scale factor.
I can't find a smooth relationship between the example points you gave, so I'm not sure how to find a single equation that would neatly and continuously map the points to each other. It looks like your solution (you said it works) might be the best: figure out which range each value maps to, and scale correspondingly.
You can just map the values:
var mapping = {
"-100": 0,
"-200": 5,
"-300": 10,
"-400": 25,
"-500": 50,
"-600": 100
}
function map_values(array){
return [mapping[array[0]], mapping[array[1]]];
}
var input = [-200,-300];
console.log(map_values(input));
I'm trying to find the technique of Normalising (not sure if that is the right word) a range of numbers.
Say I have an array:
[0, 1, 2, 3, 4, 70, 80, 900]
I want to flatten or average out the range curve, so it's more like:
[10, 11, 12, 13, 14, 50, 100, 300]. // not a real calculation
So increasing the smaller numbers in relation to reducing the larger numbers.
What is this technique called? Normalised scale? I wish to implement this in some Javascript.
UPDATE: Here is hopefully a better description of what I'm trying to do:
I have an original array of numbers:
[0, 10, 15, 50, 70, 100]
When processed through a function averageOutAllNumbers(array, slider) will produce and array that when slider is set to 100% looks like:
[0, 20, 40, 60, 80, 100] // the curve has been flattened
when slider is set to 0%, it will return the original array. If slider is set to 50%, the returned array will look something like:
[0, 12, 19, 52, 88, 100] // the curve is less steep [do not take these number literally, I'm guess what the output would be based on the slider].
the array.max() will alway be 100.
Thanks for the comments so far, they did point me closer to the solution.
No thanks to the trolls who down-voted; if you can't fix it no one can—right!
When I updated my question I realised that "increasing the smaller numbers in relation to reducing the larger numbers" would eventually lead to an evenly distributed set of numbers, e.g. [20, 20, 20, 20, 20]. However I did actually want something like I stated in the question: [0, 20, 40, 60, 80, 100] // the curve has been flattened. I did some more searching for things like:
Evenly space an array of oddly spaced numbers
Making a list of evenly spaced numbers in a certain range
Return evenly spaced numbers over a specified interval
Find what percent X is between two numbers
Amongst the list of search result I saw the answer to my original question: "What is this technique called?" Linear Interpolation
Based on that I was able to create the following:
var orig = [3, 11, 54, 72, 100];
function lerp(n, x0, x1) {
// returns a position: x that is n percent between y0 and y1
// As numbers in array are x only, y values are fixed to 0(start) - 1(end)
const y0 = 0;
const y1 = 1;
const x = ((y1 - n)*x0 + (n - y0)*x1) / (y1 - y0);
return x;
}
var flattenedEven = orig.map((value, index, arr) => {
return lerp(1, value, (Math.max(...arr)/arr.length) * (index + 1));
});
//=> [20, 40, 60, 80, 100]
var flattenedCurve = orig.map((value, index, arr) => {
return lerp(.7, value, (Math.max(...arr)/arr.length) * (index + 1));
});
//=> [14.9, 31.3, 58.2, 77.6, 100]
Say you get values anywhere from 0 to 1,000,000,000, and you want to plot 30 days. So one particular chart may have a set like:
[ 1, 465, 123, 9, ... ]
While another chart can have a set with much larger numbers:
[ 761010, 418781, ... ]
Is there an "optimal algorithm" that can take those values and segment them into "clean" numbers? Sorry for the wording, don't know the right terminology, I will try to explain.
By "optimal algorithm", I mean both in terms of minimum number of computational steps, given that it creates labels (say for the y-axis) that are simplest from a human perspective.
For example, say you always want to divide the y-axis into 5 labels. You could do this:
var max = Math.max.apply(Math, values); // 465 (from the first set of values)
var interval = max / 5;
var labels = [ interval * 0, interval * 1, interval * 2, ... ];
But that creates labels like:
[ 0, 93, 186, ... ]
And that would be complex for humans to understand. What would be better (but still not ideal) is to create labels like:
[ 0, 125, 250, 375, 500 ]
But that's still to specific. Somehow it should figure out that a better segmentation is:
[ 0, 200, 400, 600, 800 ]
That way, it's divided into more intuitive chunks.
Is there a standard way to solve this problem? What algorithm works best?
Some maths
var getLabelWidth = function(sep, max_value){
var l = (""+max_value).length;
var av = max_value/sep/Math.pow(10,l-2); // get the length max 2 digit
/// 15.22
var width = (Math.ceil(av)*Math.pow(10,l-2)); // do a ceil on the value retrieved
// and apply it to the width of max_value.
// 16 * 10 000
return width;
}
console.log(getLabelWidth(2,59)); // 30 : [0, 30, 60]
console.log(getLabelWidth(2,100)); // 50 : [0, 50, 100]
console.log(getLabelWidth(2,968)); // 490 : [0, 490, 980]
console.log(getLabelWidth(3,368)); // 130 : [0, 130, 260, 390]
console.log(getLabelWidth(3,859)); // 290 : [0, 290, 580, 870]
console.log(getLabelWidth(3,175)); // 60 : [0, 60, 120, 180]
console.log(getLabelWidth(3,580)); // 200 : [0, 200, 400, 600]
console.log(getLabelWidth(3,74)); // 25 : [0, 25, 50, 75]
console.log(getLabelWidth(4,1111)); // 300 :[0, 300, 600, 900, 1200]
console.log(getLabelWidth(4,761010)); // 200 000: [0, 200000, 400000, 600000, 800000]
It could be improved a little bit i guess,
sorry for my bad english .
For reference, here's what I ended up doing.
function computeLabels(count, max) {
var magnitude = orderOfMagnitude(max);
var multiplier = magnitude * count;
// 1
if (multiplier >= max) return buildLabels(count, multiplier);
// 2
multiplier *= 2;
if (multiplier >= max) return buildLabels(count, multiplier);
// 5
multiplier *= 5;
if (multiplier >= max) return buildLabels(count, multiplier);
// 10, don't think it will ever get here but just in case.
multiplier *= 10;
if (multiplier >= max) return buildLabels(count, multiplier);
}
function buildLabels(count, multiplier) {
var labels = new Array(count);
while (count--) labels[count] = formatLabel(count * multiplier);
return labels;
}
function formatLabel(value) {
if (value > 10e5) return (value / 10e5) + 'M'; // millions
if (value > 10e2) return (value / 10e2) + 'K'; // thousands
return value; // <= hundreds
}
function orderOfMagnitude(val) {
var order = Math.floor(log10(val) + 0.000000001);
return Math.pow(10, order);
}
After drawing it out on paper, the "desirable" labels seemed to follow a simple pattern:
Find the max value in the set.
Get the order of magnitude for it.
Multiply the order of magnitude by the number of ticks.
Iterate: If that previous calculation is greater than the max value, then use it. Otherwise, multiply the value times 2 and check. If not, try times 5. So the pattern is, 1, 2, 5.
This gives you labels that are like:
10, 20 (2 ticks)
20, 40
50, 100
100, 200
200, 400
500, 1000
...
10, 20, 30 (3 ticks)
20, 40, 60
50, 100, 150 (don't like this one too much but oh well)
100, 200, 300
10, 20, 30, 40 (4 ticks)
...
It seems like it can be improved, both in producing better quality "human readable" labels, and in using more optimized functionality, but don't quite see it yet. This works for now.
Would love to know if you find a better way!