20

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));
});
cdarwin
  • 4,141
  • 9
  • 42
  • 66

6 Answers6

2

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));
});
ConnorsFan
  • 70,558
  • 13
  • 122
  • 146
2

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));
TheChetan
  • 4,440
  • 3
  • 32
  • 41
  • the idea of using log(x+1) is an interesting one, but I don't fully understand why are you multiplying by (max+min)/(max-min). I'd also like to have lineLen equals to maxLineLength for the element equal to the max. – cdarwin Nov 13 '17 at 20:04
  • It's just a scaling factor. You can choose to omit it. But it helps when max and min are closer together. And if they are far apart, then it does nothing. It scales the difference between the closer terms when the values are close to each other. Try the example for `[1,2,3,4]` – TheChetan Nov 14 '17 at 03:05
  • @cdarwin, edited the answer to suit your requirements. Which is just `x+1` instead of `x` and `max+1` instead of `max`. – TheChetan Nov 15 '17 at 16:59
0

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; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • this leaves one... one, but I have in mind that it should be scaled as well to something like maxLineLength / Math.log(max) – cdarwin Nov 08 '17 at 21:39
  • I'm still thinking about one, however if you map an event like 0.1 it becomes a negative number (-38) , while I need it to be mapped to a small but positive number. – cdarwin Nov 09 '17 at 16:36
  • please add some example data and the wanted outcome. – Nina Scholz Nov 09 '17 at 16:37
  • I edited my question, specifing positive numbers should remain positive. Maybe an horizontal translation is needed – cdarwin Nov 09 '17 at 16:42
0

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>
  • Unfortunately, I have no idea how to express this method in a mathematical way. – Константин Ван Nov 10 '17 at 21:20
  • I think shifting is needed, at least to avoid negative results. But I don't understand why you shift by Math.E when you calculate the var adjusted, can you explain why? – cdarwin Nov 15 '17 at 16:32
  • @cdarwin By this algorithm, the smaller the logarithmic base you specified is, the more they are _adjusted_ (try `0.00000001` for the base). You may specify any value for the base. I just thought that `Math.E` would be great for the example code because it's one of the _beautiful_ constants, such as π. – Константин Ван Nov 16 '17 at 05:06
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!

Sam H.
  • 4,091
  • 3
  • 26
  • 34
0

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
lemarc
  • 111
  • 1
  • 3