Calculating the length of a segment in a logarithmic scale - javascript
I want to calculate the length of a line for a series of events.
I'm doing this with the following code.
var maxLineLength = 20;
var lineLen = function(x, max) {
return maxLineLength * (x / max);
}
var events = [0.1, 1, 5, 20, 50];
var max = Math.max.apply(null, events);
events.map(function (x) {
console.log(lineLen(x, max));
});
This works, but I'm using linear scaling, while I'd like to use logarithms, because I don't want small events to become too small numbers when big ones are present.
I modified the lineLen function as you can see below, but - obviously - it doesn't work for events equals to one, because the log of one is zero. I want to show events equals to one (opportunely scaled) and not make them become zero. I also need positive numbers to remain positive (0.1 becomes a negative number)
How should I modify lineLen to use a logarithmic scale?
var maxLineLength = 20;
var lineLen = function(x, max) {
return maxLineLength * (Math.log(x) / Math.log(max));
}
var events = [0.1, 1, 5, 20, 50];
var max = Math.max.apply(null, events);
events.map(function (x) {
console.log(lineLen(x, max));
});
You can take log(x+1) instead of log(x), that doesn't change the value too much and the ratios are maintained for smaller numbers.
var maxLineLength = 20;
var lineLen = (x, max) => maxLineLength * Math.log(x+1)/Math.log(max+1);
var events = [ 0.1, 1, 5, 20, 50];
var visualizer = function(events){
var max = Math.max.apply(null, events);
return events.reduce((y, x) => {
y.push(lineLen(x, max));
return y;
}, []);
};
console.log(visualizer(events));
You can use an expression like Math.pow(x, 0.35) instead of Math.log(x). It keeps all values positive, and gives the behavior that you want for small ratios. You can experiment with different exponent values in the range (0,1) to find the one that fits your needs.
var maxLineLength = 20;
var exponent = 0.35;
var lineLen = function(x, max) {
return maxLineLength * Math.pow(x/max, exponent);
}
var events = [0, 0.01, 0.1, 1, 5, 20, 50];
var max = Math.max.apply(null, events);
events.map(function (x) {
console.log(lineLen(x, max));
});
You could decrement maxLineLength and add one at the end of the calculation.
For values smaller than one, you could use a factor which normalizes all values relative to the first value. The start value is always one, or in terms of logaritmic view, zero.
var maxLineLength = 20,
lineLen = function(max) {
return function (x) {
return (maxLineLength - 1) * Math.log(x) / Math.log(max) + 1;
};
},
events = [0.1, 1, 5, 20, 50],
normalized = events.map((v, _, a) => v / a[0]),
max = Math.max.apply(null, normalized),
result = normalized.map(lineLen(max));
console.log(result);
console.log(normalized);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You are scaling these numbers. Your starting set is the domain, what you end up with is the range. The shape of that transformation, it sounds like, will be either log or follow a power.
It turns out this is a very common problem in many fields, especially data visualization. That is why D3.js - THE data visualization tool - has everything you need to do this easily.
const x = d3.scale.log().range([0, events]);
It's the right to for the job. And if you need to do some graphs, you're all set!
Shorter but not too short; longer but not too long.
Actually I met the same problem years ago, and gave up for this (maybe?).
I've just read your question here and now, and I think I've just found the solution: shifting.
const log = (base, value) => (Math.log(value) / Math.log(base));
const weights = [0, 0.1, 1, 5, 20, 50, 100];
const base = Math.E; // Setting
const shifted = weights.map(x => x + base);
const logged = shifted.map(x => log(base, x));
const unshifted = logged.map(x => x - 1);
const total = unshifted.reduce((a, b) => a + b, 0);
const ratio = unshifted.map(x => x / total);
const percents = ratio.map(x => x * 100);
console.log(percents);
// [
// 0,
// 0.35723375538333857,
// 3.097582209424984,
// 10.3192042142806,
// 20.994247877004888,
// 29.318026542735115,
// 35.91370540117108
// ]
Visualization
The smaller the logarithmic base is, the more they are adjusted;
and vice versa.
Actually I don't know the reason. XD
<!DOCTYPE html>
<html>
<head>
<meta name="author" content="K.">
<title>Shorter but not too short; longer but not too long.</title>
<style>
canvas
{
background-color: whitesmoke;
}
</style>
</head>
<body>
<canvas id="canvas" height="5"></canvas>
<label>Weights: <input id="weights" type="text" value="[0, 0.1, 100, 1, 5, 20, 2.718, 50]">.</label>
<label>Base: <input id="base" type="number" value="2.718281828459045">.</label>
<button id="draw" type="button">Draw</button>
<script>
const input = new Proxy({}, {
get(_, thing)
{
return eval(document.getElementById(thing).value);
}
});
const color = ["tomato", "black"];
const canvas_element = document.getElementById("canvas");
const canvas_context = canvas_element.getContext("2d");
canvas_element.width = document.body.clientWidth;
document.getElementById("draw").addEventListener("click", _ => {
const log = (base, value) => (Math.log(value) / Math.log(base));
const weights = input.weights;
const base = input.base;
const orig_total = weights.reduce((a, b) => a + b, 0);
const orig_percents = weights.map(x => x / orig_total * 100);
const adjusted = weights.map(x => x + base);
const logged = adjusted.map(x => log(base, x));
const rebased = logged.map(x => x - 1);
const total = rebased.reduce((a, b) => a + b, 0);
const ratio = rebased.map(x => x / total);
const percents = ratio.map(x => x * 100);
const result = percents.map((percent, index) => `${weights[index]} | ${orig_percents[index]}% --> ${percent}% (${color[index & 1]})`);
console.info(result);
let position = 0;
ratio.forEach((rate, index) => {
canvas_context.beginPath();
canvas_context.moveTo(position, 0);
position += (canvas_element.width * rate);
canvas_context.lineTo(position, 0);
canvas_context.lineWidth = 10;
canvas_context.strokeStyle = color[index & 1];
canvas_context.stroke();
});
});
</script>
</body>
</html>
As long as your events are > 0 you can avoid shifting by scaling the events so the minimum value is above 1. The scalar can be calculated based on a minimum line length in addition to the maximum line length you already have.
// generates an array of line lengths between minLineLength and maxLineLength
// assumes events contains only values > 0 and 0 < minLineLength < maxLineLength
function generateLineLengths(events, minLineLength, maxLineLength) {
var min = Math.min.apply(null, events);
var max = Math.max.apply(null, events);
//calculate scalar that sets line length for the minimum value in events to minLineLength
var mmr = minLineLength / maxLineLength;
var scalar = Math.pow(Math.pow(max, mmr) / min, 1 / (1 - mmr));
function lineLength(x) {
return maxLineLength * (Math.log(x * scalar) / Math.log(max * scalar));
}
return events.map(lineLength)
}
var events = [0.1, 1, 5, 20, 50];
console.log('Between 1 and 20')
generateLineLengths(events, 1, 20).forEach(function(x) {
console.log(x)
})
// 1
// 8.039722549519123
// 12.960277450480875
// 17.19861274759562
// 20
console.log('Between 5 and 25')
generateLineLengths(events, 5, 25).forEach(function(x) {
console.log(x)
})
// 5
// 12.410234262651711
// 17.58976573734829
// 22.05117131325855
// 25
Related
javascript change position of element without string concatenation
Most of the answers to this question give solution similar to: let x_pos=42, y_pos=43; var d = document.getElementById('yourDivId'); d.style.position = "absolute"; d.style.left = x_pos+'px'; d.style.top = y_pos+'px'; but isn't it expensive, just to change the postion, to convert the integers to string and do some concatenations? I think the browser will probably do exactly the reverse calculations to get the integer values again.
It's not that expensive to concatenate strings in modern browsers nowadays. Here's a factory to move an element to a(nother) position, using a small helper for converting numbers to units. // factory to avoid recreating [number2Unit] for every call and // and delegate the actual styling to small methods. function moveElToFactory(ux = `px`, uy = `px`) { const number2Unit = (nr, unit) => `${nr}${unit}`; const moveX = (el, x, unit) => el.style.left = number2Unit(x, unit); const moveY = (el, y, unit) => el.style.top = number2Unit(y, unit); return (el, x, y, uX = ux, uY = uy) => { el.style.position = `absolute`; y && moveY(el, y, uY); x && moveX(el, x, uX); } } // demo const moveElTo = moveElToFactory(); setTimeout(() => { const [d1, d2, d3] = [ document.querySelector('#div1'), document.querySelector('#div2'), document.querySelector('#div3')] moveElTo(d1, 42, 43); d1.textContent += ` [x, y] ${d1.style.left}, ${d1.style.top}`; moveElTo(d2, 15, 50, `vw`, `%`); d2.textContent += ` [x, y] ${d2.style.left}, ${d2.style.top}`; moveElTo(d3, ...[,35,,`%`]); d3.textContent += ` [x, y] 0, ${d3.style.top}`; }, 1000); <div id="div1">where am I (div#div1)?</div> <div id="div2">And where am I (div#div2)?</div> <div id="div3">And I (div#div3) am where?</div>
Decreasing variable influence over each iteration
I have a game in which I'm creating bonus items based on a frequency variable. I would like the generation frequency to become shorter over time. I know how to do that, simply subtract a small number from the frequency every second. Let frequency = 100; function every1sec { frequency -= 0.1; } But as I'm not very mathematically talented, this is the part where I have trouble: What is a good way to slow down the rate of the frequency becoming shorter over time? So if in the beginning the bonus generation frequency becomes 1 second shorter every minute, how can I make it only become 0.01 seconds shorter after ten minutes have passed. And the part I really have trouble with is this: how can I make the rate of decrease follow a smooth easing curve (instead of a linear decrease). I don't know the right terminology to search an answer for this. I tried searching for something like "decrease algorithm influence over time" but that didn't get me any useful information.
On a simple but rudimentary way you can replace that 0.1 with a variable which you can also modify with time: let frequency = 100; let quantity = 0.1; function every1sec() { frequency -= quantity; quantity -= 0.01; } But this will be a little bit junky and should require a lot of tweaking to obtain the desired behaviour. If you want something smooth you can use JS and mathematical functions which will return points in a curve, like sine and cosine. const MAX_FREQUENCY = 100; let frequency = MAX_FREQUENCY; let modifier = 1; function every1sec() { // This way, frequency will be in range [0, MAX_FREQUENCY] modifier = Math.min(modifier - 0.1, 0); frequency = MAX_FREQUENCY * Math.sin(modifier); } Even further, you can customize the whole method: const STEPS = 30; // Number of times the function needs to be called to reach MIN_FREQUCNY const MIN_FREQUENCY = 40; const MAX_FREQUENCY = 100; const FREQUENCY_RANGE = MAX_FREQUENCY - MIN_FREQUENCY; // 100 - 40 = 60 // ±Math.PI / 2 because Math.sin will be used const MIN_MODIFIER = -Math.PI / 2; const MAX_MODIFIER = Math.PI / 2; const MODIFIER_RANGE = MAX_MODIFIER - MIN_MODIFIER; const MODIFIER_STEP = MODIFIER_RANGE / STEPS; let frequency = MAX_FREQUENCY; let modifier = MAX_MODIFIER; // This function can be modified to return different types of curves // without modifying every1sec function function curveFunction(modifierValue) { // 1 + Math.sin(x) so it will be in range [0, 2] instead of [-1, 1] return 1 + Math.sin(modifierValue); } function every1sec () { modifier = Math.max(modifier - MODIFIER_STEP, MIN_MODIFIER); // FREQUENCY_RANGE * 0.5 because it will be multiplied by a coefficient in range [0, 2] frequency = MIN_FREQUENCY + FREQUENCY_RANGE * 0.5 * curveFunction(modifier) ; } // The only purpose of this loop is to provide the results of this example for (let i = 0; i <= STEPS; i++) { let li = document.createElement('li'); li.innerHTML = frequency; document.getElementsByTagName('ul')[0].appendChild(li); every1sec(); } <ul></ul> The last example will modify the frequency starting at a value of 100 and, every second during 30 seconds (steps), updating it to the correspondent value in the range [100, 40] following a sine curve.
To get an exponential decrease, multiply by a fraction. let frequency = 100; function every1sec() { frequency *= 0.99; } let interval = setInterval(function() { every1sec(); console.log(frequency.toFixed(1)) if (frequency < 50) { clearInterval(interval); } }, 1000);
is there any way we can append last value forcefully on x-axis?
I have min and max value for x-axis. If my minvalue=0 and maxvalue=23 i want my x-axis scale to range from 0-23 but due to the automatic tickinterval adjustment(eg. 2) my x-axis scale ranges from 0-24. how do i restrict it to 23 ?
You can use tickPositions or tickPositioner option, for example: const breaks = 10; const dataMin = 0; const dataMax = 23; const step = Math.round((dataMax - dataMin) / breaks * 100) / 100; Highcharts.chart('container', { ..., xAxis: { tickPositioner: function() { const positions = []; for (var i = dataMin; i < (dataMax - step / 2); i += step) { positions.push(Math.round(i * 100) / 100); } positions.push(dataMax); return positions; } } }); Live demo: http://jsfiddle.net/BlackLabel/6m4e8x0y/4927/ API Reference: https://api.highcharts.com/highcharts/xAxis.tickPositions https://api.highcharts.com/highcharts/xAxis.tickPositioner
Calculation of scaling factor
I've got multiple number values with a min and a max value. Assume the minimum value should be visualized by a 5px circle and the maximum value should be visualized by a 50px circle, I need to calculate a scaling factor for all other values. 50 / maxValue * value ...is not working well, because it does not consider the min value. const array = [5, 20, 50, 100] const min = 5 // => 5px const max = 100 // => 50px
d3-scale does exactly this for you. You just need to create a scaleLinear, define the domain (range of input values), and range (corresponding desired output values). Have a look at the snippet below for an illustration: const array = [5, 20, 50, 100] const min = 5 // => 5px const max = 100 // => 50px // define the scaling function const size = d3.scaleLinear() .domain([min, max]) .range([5, 50]) // call it for each value in the input array array.forEach(function(val) { console.log(val, Math.round(size(val)) + 'px') }) <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/d3-scale#3.2.0/dist/d3-scale.min.js"></script>
const array = [5, 20, 50, 100] const minpx = 5 // => 5px const maxpx = 50 // => 50px let max = Math.max(...array) let min = Math.min(...array) // (x-min)/(max-min) the ratio // * (maxpx-minpx) the new range // + minpx the new offset let transform = x=>(x-min)/(max-min)*(maxpx-minpx)+minpx console.log(array.map(transform))
Use the so called Linear Interpolation method. The following function takes 3 parameters: a: The starting value b: The destination value amount: The normal or actual value (percentage between 0 and 1) to control the Linear Interpolation function mix(a, b, amount) { return (1 - amount) * a + amount * b } Imagine a line from A to B and you put a circle on it: A ----o--------------------------- B If your normal value is equal to 1 the circle will instantly switch from A to B. If your normal value is equal to 0 the circle will not move. The closer your normal is to 0 the smoother will be the interpolation. The closer your normal is to 1 the sharper will be the interpolation. const array = [5, 20, 50, 100] const minpx = 5 // => 5px const maxpx = 50 // => 50px const min = Math.min(...array) // 5 const max = Math.max(...array) // 100 function mix(a, b, amount) { return (1 - amount) * a + amount * b } array.map(value => mix(minpx, maxpx, (value-min) / (max-min))) // [ 5, 12.105263157894736, 26.31578947368421, 50 ]
gRaphael linechart draws values beyond axis
When drawing a linechart with gRaphael using milliseconds along the x-axis I commonly get inconsistencies in the placement of the data points. Most commonly the initial data points are to the left of the y-axis (as seen in the fiddle below), sometimes the last data-point will be beyond the right side of the view-box/past the termination of the x-axis. Does anyone know: 1) Why this occurs, 2) How to prevent it, &/or 3) How to check for it (I can use transform to move the lines/points if I know when it has happened/by how much). my code: var r = Raphael("holder"), txtattr = { font: "12px sans-serif" }; var r2 = Raphael("holder2"), txtattr2 = { font: "12px sans-serif" }; var x = [], y = [], y2 = [], y3 = []; for (var i = 0; i < 1e6; i++) { x[i] = i * 10; y[i] = (y[i - 1] || 0) + (Math.random() * 7) - 3; } var demoX = [[1, 2, 3, 4, 5, 6, 7],[3.5, 4.5, 5.5, 6.5, 7, 8]]; var demoY = [[12, 32, 23, 15, 17, 27, 22], [10, 20, 30, 25, 15, 28]]; var xVals = [1288885800000, 1289929440000, 1290094500000, 1290439560000, 1300721700000, 1359499228000, 1359499308000, 1359499372000]; var yVals = [80, 76, 70, 74, 74, 78, 77, 72]; var xVals2 = [1288885800000, 1289929440000]; var yVals2 = [80, 76]; var lines = r.linechart(10, 10, 300, 220, xVals, yVals, { nostroke: false, axis: "0 0 1 1", symbol: "circle", smooth: true }) .hoverColumn(function () { this.tags = r.set(); for (var i = 0, ii = this.y.length; i < ii; i++) { this.tags.push(r.tag(this.x, this.y[i], this.values[i], 160, 10).insertBefore(this).attr([{ fill: "#fff" }, { fill: this.symbols[i].attr("fill") }])); } }, function () { this.tags && this.tags.remove(); }); lines.symbols.attr({ r: 3 }); var lines2 = r2.linechart(10, 10, 300, 220, xVals2, yVals2, { nostroke: false, axis: "0 0 1 1", symbol: "circle", smooth: true }) .hoverColumn(function () { this.tags = r2.set(); for (var i = 0, ii = this.y.length; i < ii; i++) { this.tags.push(r.tag(this.x, this.y[i], this.values[i], 160, 10).insertBefore(this).attr([{ fill: "#fff" }, { fill: this.symbols[i].attr("fill") }])); } }, function () { this.tags && this.tags.remove(); }); lines2.symbols.attr({ r: 3 }); I do have to use gRaphael and the x-axis has to be in milliseconds (it is labeled later w/customized date strings) Primary example fiddle: http://jsfiddle.net/kcar/aNJxf/ Secondary example fiddle (4th example on page frequently shows both axis errors): http://jsfiddle.net/kcar/saBnT/ root cause is the snapEnds function (line 718 g.raphael.js), the rounding it does, while fine in some cases, is adding or subtracting years from/to the date in other cases. Haven't stepped all the way through after this point, but since the datapoints are misplaced every time the rounding gets crazy and not when it doesn't, I'm going to go ahead and assume this is causing issues with calculating the chart columns, also before being sent to snapEnds the values are spot on just to confirm its not just receiving miscalculated data. code of that function from g.raphael.js snapEnds: function(from, to, steps) { var f = from, t = to; if (f == t) { return {from: f, to: t, power: 0}; } function round(a) { return Math.abs(a - .5) < .25 ? ~~(a) + .5 : Math.round(a); } var d = (t - f) / steps, r = ~~(d), R = r, i = 0; if (r) { while (R) { i--; R = ~~(d * Math.pow(10, i)) / Math.pow(10, i); } i ++; } else { if(d == 0 || !isFinite(d)) { i = 1; } else { while (!r) { i = i || 1; r = ~~(d * Math.pow(10, i)) / Math.pow(10, i); i++; } } i && i--; } t = round(to * Math.pow(10, i)) / Math.pow(10, i); if (t < to) { t = round((to + .5) * Math.pow(10, i)) / Math.pow(10, i); } f = round((from - (i > 0 ? 0 : .5)) * Math.pow(10, i)) / Math.pow(10, i); return { from: f, to: t, power: i }; },
removed the rounding nonsense from snapEnds and no more issues, not noticed any downside from either axis or any other area of the chart. If you see one I'd love to hear it though. code of that function from g.raphael.js now: snapEnds: function(from, to, steps) { return {from: from, to: to, power: 0}; },
Hi if you comment this: if (valuesy[i].length > width - 2 * gutter) { valuesy[i] = shrink(valuesy[i], width - 2 * gutter); len = width - 2 * gutter; } if (valuesx[i] && valuesx[i].length > width - 2 * gutter) { valuesx[i] = shrink(valuesx[i], width - 2 * gutter); } in g.line.js, It seems to solve the problem, and it also solves a similar problem with the values in the y axis.
Upgrading from v0.50 to v0.51 fixed the issue for me.
Still not sure why it occurs, adding in a transparent set was not a desirable option. The simplest way to check for if the datapoints were rendered outside of the graph seems to be getting a bounding box for the axis set and a bounding box for the datapoints and checking the difference between the x and x2 values. If anyone can help me with scaling the datapoint set, or figure out how to make this not happen at all, I will still happily appreciate/up vote answers //assuming datapoints is the Raphael Set for the datapoints, axes is the //Raphael Set for the axis, and datalines is the Raphael Set for the //datapoint lines var pointsBBox = datapoints.getBBox(); var axesBBox = axes.getBBox(); var xGapLeft = Math.ceil(axesBBox.x - pointsBBox.x); //rounding up to integer to simplify, and the extra boost from y-axis doesn't //hurt, <1 is a negligible distance in transform var xGapRight = Math.ceil(axesBBox.x2 - pointsBBox.x2); var xGap = 0; if(xGapLeft > 0){ datapoints.transform('t' +xGapLeft +',0'); datalines.transform('t' +xGapLeft +',0'); xGap = xGapLeft; }else if (xGapRight < 0) { //using else if because if it is a scale issue it will //be too far right & too far left, meaning both are true and using transform will //just shift it right then left and you are worse off than before, using //set.transform(scale) works great on dataline but when using on datapoints scales // symbol radius not placement if (xGapLeft < 0 && xGapRight < xGapLeft) { xGapRight = xGapLeft; } //in this case the initial point is right of y-axis, the end point is right of //x-axis termination, and the difference between last point/axis is greater than //difference between first point/axis datapoints.transform('t' +xGapRight +',0'); datalines.transform('t' +xGapRight +',0'); xGap = xGapRight; } rehookHoverOverEvent(xGap); //there are so many ways to do this just leaving it //here as a call to do so, if you don't the hover tags will be over the original //datapoints instead of the new location, at least they were in my case.