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 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!
I've got a animated circle which looks like this:
the blue part counts down, so for example from full to nothing in 10 seconds. The orange circle is just a circle. But I want that the circle will be smaller when you click on it. So i made an onclick event for the circle.
circleDraw.node.onclick = function () {
circleDraw.animate({
stroke: "#E0B6B2",
arc: [100, 100, 100, 100, 100]
}, 500);
circleDraw.toFront();
};
That works, i've made it for both of the circles, they both become smaller but, After the 500 mili seconds the blue circle becomes big again because the timer for the blue circle got the parameters that it should be bigger.
circleDraw.animate({
arc: [100, 100, 0, 100, 500]
}, 10000);
Because the blue circle counts for 10 seconds it becomes automatically bigger again. How do I make both circles smaller but keep the timer counting down?
I was thinking of stopping the animation for the blue circle and save the remaining mili seconds of the animation draw it again smaller and start the animation again with the remaining seconds, but I don't know how to do this. But maybe i'm looking in the wrong direction and do I have to make it different.
Thanks.
All my code:
/************************************************************************/
/* Raphael JS magic
*************************************************************************/
var drawTheCircleVector = function(xloc, yloc, value, total, R) {
var alpha = 360 / total * value,
a = (90 - alpha) * Math.PI / 180,
x = xloc + R * Math.cos(a),
y = yloc - R * Math.sin(a),
path;
if (total == value) {
path = [
["M", xloc, yloc - R],
["A", R, R, 0, 1, 1, xloc - 0.01, yloc - R]
];
} else {
path = [
["M", xloc, yloc - R],
["A", R, R, 0, +(alpha > 180), 1, x, y]
];
}
return {
path: path
};
}; /************************************************************************/
/* Make the circles
*************************************************************************/
var timerCircle = Raphael("timer", 320, 320);
var circleBg = Raphael("backgroundCircle", 320, 320);
timerCircle.customAttributes.arc = drawTheCircleVector
circleBg.customAttributes.arc = drawTheCircleVector
/************************************************************************/
/* draw the circles
*************************************************************************/
var drawMe = circleBg.path().attr({
"fill": "#FF7F66",
"stroke": 0,
arc: [160, 160, 100, 100, 140]
});
var clickOnes = true;
drawMe.node.onclick = function() {
if (clickOnes == true) {
circleDraw.animate({
arc: [100, 100, 0, 100, 100]
}, 500);
circleDraw.toFront();
drawMe.animate({
arc: [100, 100, 100, 100, 100]
}, 500);
circleDraw.toFront();
clickOnes = false;
} else {
circleDraw.animate({
arc: [160, 160, 0, 100, 150]
}, 500);
circleDraw.toFront();
drawMe.animate({
arc: [160, 160, 100, 100, 140]
}, 500);
circleDraw.toFront();
clickOnes = true;
}
};
// arc: [Xposition, Yposition, how much 1 = begin 100 = end, ? = 100, 150];
/************************************************************************/
/* Draw the circle
*************************************************************************/
var circleDraw = timerCircle.path().attr({
"stroke": "#2185C5",
"stroke-width": 10,
arc: [160, 160, 100, 100, 150]
});
circleDraw.animate({
arc: [160, 160, 0, 100, 150]
}, 9000);
window.setInterval(function() {
goToNextStopGardensPointBus210()
}, 9000);
Here is my code the timer works and if you click on the circle it will become smaller but if you click on it before the bue cirlce is done it will become big again.
UPDATE
working version of what I got on jsFiddle,
http://jsfiddle.net/hgwd92/2S4Dm/
Here's a fiddle for you..
The issue was you where redrawing the items, with new animation speeds and such. Using the transform function, you can add a scaling transform that acts independent of the draws.
circleDraw.animate({transform: 'S0.5,0.5,160,160', 'stroke-width': 5}, 500);
and then to set it back...
circleDraw.animate({transform: 'S1,1,160,160', 'stroke-width': 10}, 500);
Note you need to set the center for the blue arc (the 160, 160), or once it gets past half way the center of the arc will move and it will scale to a different position.
EDIT: Updated the fiddle and code to scale the blue line to so it looks better.